﻿2026-09-06T13:09:01.6614400Z ##[group]Run ./traceable-reqs lint || true
2026-09-06T13:09:01.6614544Z [36;1m./traceable-reqs lint || true[0m
2026-09-06T13:09:01.6627029Z shell: /usr/bin/bash -e {0}
2026-09-06T13:09:01.6627129Z ##[endgroup]
2026-09-06T13:09:01.7406964Z Requirement quality findings (1352); 876 requirements queued for agent review:
2026-09-06T13:09:01.7408327Z   [must] requirement_quality REQ-ACCESS-RETIRED-VERB-REFUSAL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7409947Z   [must] requirement_quality REQ-ACCESS-RETIRED-VERB-REFUSAL criterion=length — title is 459 words; want 3..=25
2026-09-06T13:09:01.7411104Z   [must] requirement_quality REQ-ACCESS-VERDICT-REASON criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7412205Z   [must] requirement_quality REQ-ACCESS-VERDICT-REASON criterion=length — title is 391 words; want 3..=25
2026-09-06T13:09:01.7413371Z   [must] requirement_quality REQ-ACL-ACCESS-REFRESH-ER-ONLY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7414573Z   [must] requirement_quality REQ-ACL-ACCESS-REFRESH-ER-ONLY criterion=length — title is 195 words; want 3..=25
2026-09-06T13:09:01.7415674Z   [must] requirement_quality REQ-ACL-ACCESS-REFRESH-VERB criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7416796Z   [must] requirement_quality REQ-ACL-ACCESS-REFRESH-VERB criterion=length — title is 165 words; want 3..=25
2026-09-06T13:09:01.7418031Z   [must] requirement_quality REQ-ACL-DISCOVER-DEFAULT-ON criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7419221Z   [must] requirement_quality REQ-ACL-DISCOVER-DEFAULT-ON criterion=length — title is 489 words; want 3..=25
2026-09-06T13:09:01.7420338Z   [must] requirement_quality REQ-ACL-DISCOVER-GATE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7421426Z   [must] requirement_quality REQ-ACL-DISCOVER-GATE criterion=length — title is 215 words; want 3..=25
2026-09-06T13:09:01.7423267Z   [must] requirement_quality REQ-ACL-ER-DISCOVER-CONJUNCTION-NOTICE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7424546Z   [must] requirement_quality REQ-ACL-ER-DISCOVER-CONJUNCTION-NOTICE criterion=length — title is 610 words; want 3..=25
2026-09-06T13:09:01.7425734Z   [must] requirement_quality REQ-ACL-FAIL-CLOSED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7426802Z   [must] requirement_quality REQ-ACL-FAIL-CLOSED criterion=length — title is 401 words; want 3..=25
2026-09-06T13:09:01.7428014Z   [must] requirement_quality REQ-ACL-FORK-WITHOUT-DISCOVER-CONSEQUENCE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7429400Z   [must] requirement_quality REQ-ACL-FORK-WITHOUT-DISCOVER-CONSEQUENCE criterion=length — title is 511 words; want 3..=25
2026-09-06T13:09:01.7430649Z   [must] requirement_quality REQ-ACL-GRANT-NODES-POLICY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7431771Z   [must] requirement_quality REQ-ACL-GRANT-NODES-POLICY criterion=length — title is 263 words; want 3..=25
2026-09-06T13:09:01.7432906Z   [must] requirement_quality REQ-ACL-INTRA-NODE-SELF criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7434030Z   [must] requirement_quality REQ-ACL-INTRA-NODE-SELF criterion=length — title is 676 words; want 3..=25
2026-09-06T13:09:01.7435173Z   [must] requirement_quality REQ-ACL-LOCAL-ORIGIN-TIERS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7436289Z   [must] requirement_quality REQ-ACL-LOCAL-ORIGIN-TIERS criterion=length — title is 906 words; want 3..=25
2026-09-06T13:09:01.7437495Z   [must] requirement_quality REQ-ACL-LOCAL-SUBJECT-ONE-SHAPE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7438483Z   [must] requirement_quality REQ-ACL-LOCAL-SUBJECT-ONE-SHAPE criterion=length — title is 590 words; want 3..=25
2026-09-06T13:09:01.7439303Z   [must] requirement_quality REQ-ACL-LOCKED-POSTURE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7440069Z   [must] requirement_quality REQ-ACL-LOCKED-POSTURE criterion=length — title is 628 words; want 3..=25
2026-09-06T13:09:01.7440805Z   [must] requirement_quality REQ-ACL-MODE-ADVISORY-GOSSIP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7441519Z   [must] requirement_quality REQ-ACL-MODE-ADVISORY-GOSSIP criterion=length — title is 134 words; want 3..=25
2026-09-06T13:09:01.7442255Z   [must] requirement_quality REQ-ACL-MUTATION-HUSK-REFUSAL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7442990Z   [must] requirement_quality REQ-ACL-MUTATION-HUSK-REFUSAL criterion=length — title is 493 words; want 3..=25
2026-09-06T13:09:01.7443687Z   [must] requirement_quality REQ-ACL-NODE-MODE-SET criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7444349Z   [must] requirement_quality REQ-ACL-NODE-MODE-SET criterion=length — title is 163 words; want 3..=25
2026-09-06T13:09:01.7445079Z   [must] requirement_quality REQ-ACL-NODE-VIEW criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7445728Z   [must] requirement_quality REQ-ACL-NODE-VIEW criterion=length — title is 124 words; want 3..=25
2026-09-06T13:09:01.7446395Z   [must] requirement_quality REQ-ACL-ORIGIN-QUALIFIER criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7447081Z   [must] requirement_quality REQ-ACL-ORIGIN-QUALIFIER criterion=length — title is 355 words; want 3..=25
2026-09-06T13:09:01.7447832Z   [must] requirement_quality REQ-ACL-POSITIONAL-ALLOW-HONORS-FLAGS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7448609Z   [must] requirement_quality REQ-ACL-POSITIONAL-ALLOW-HONORS-FLAGS criterion=length — title is 666 words; want 3..=25
2026-09-06T13:09:01.7449487Z   [must] requirement_quality REQ-ACL-PRECISE-ALLOW-WRITE-CONTRACT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7450257Z   [must] requirement_quality REQ-ACL-PRECISE-ALLOW-WRITE-CONTRACT criterion=length — title is 854 words; want 3..=25
2026-09-06T13:09:01.7450987Z   [must] requirement_quality REQ-ACL-RC-VIEW-SPLIT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7451651Z   [must] requirement_quality REQ-ACL-RC-VIEW-SPLIT criterion=length — title is 164 words; want 3..=25
2026-09-06T13:09:01.7452321Z   [must] requirement_quality REQ-ACL-RULE-MUTATION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7452987Z   [must] requirement_quality REQ-ACL-RULE-MUTATION criterion=length — title is 346 words; want 3..=25
2026-09-06T13:09:01.7453659Z   [must] requirement_quality REQ-ACL-SUBJECT-CHAIN criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7454326Z   [must] requirement_quality REQ-ACL-SUBJECT-CHAIN criterion=length — title is 511 words; want 3..=25
2026-09-06T13:09:01.7455027Z   [must] requirement_quality REQ-ACL-SUBNET-MODE-CAPTURE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7456131Z   [must] requirement_quality REQ-ACL-SUBNET-MODE-CAPTURE criterion=length — title is 193 words; want 3..=25
2026-09-06T13:09:01.7457490Z   [must] requirement_quality REQ-ACL-SURFACE-ATTRIBUTABILITY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7458703Z   [must] requirement_quality REQ-ACL-SURFACE-ATTRIBUTABILITY criterion=length — title is 236 words; want 3..=25
2026-09-06T13:09:01.7460738Z   [must] requirement_quality REQ-ACL-SURFACE-DESCRIPTION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7461887Z   [must] requirement_quality REQ-ACL-SURFACE-DESCRIPTION criterion=length — title is 319 words; want 3..=25
2026-09-06T13:09:01.7463184Z   [must] requirement_quality REQ-ACL-SURFACE-MODE-VERB criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7464439Z   [must] requirement_quality REQ-ACL-SURFACE-MODE-VERB criterion=length — title is 553 words; want 3..=25
2026-09-06T13:09:01.7465340Z   [must] requirement_quality REQ-ACL-SURFACE-VOCAB criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7466013Z   [must] requirement_quality REQ-ACL-SURFACE-VOCAB criterion=length — title is 255 words; want 3..=25
2026-09-06T13:09:01.7466720Z   [must] requirement_quality REQ-ACL-VIEW-DRILLDOWN criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7467401Z   [must] requirement_quality REQ-ACL-VIEW-DRILLDOWN criterion=length — title is 161 words; want 3..=25
2026-09-06T13:09:01.7468088Z   [must] requirement_quality REQ-ACL-VIEW-ROSTER criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7468885Z   [must] requirement_quality REQ-ACL-VIEW-ROSTER criterion=length — title is 209 words; want 3..=25
2026-09-06T13:09:01.7469583Z   [must] requirement_quality REQ-ACTIVITY-INFO-PULL criterion=length — title is 45 words; want 3..=25
2026-09-06T13:09:01.7470264Z   [must] requirement_quality REQ-ACTIVITY-LINK-PUSH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7470936Z   [must] requirement_quality REQ-ACTIVITY-LINK-PUSH criterion=length — title is 169 words; want 3..=25
2026-09-06T13:09:01.7471536Z   [must] requirement_quality REQ-ACTIVITY-LIST-JSON criterion=length — title is 46 words; want 3..=25
2026-09-06T13:09:01.7472251Z   [must] requirement_quality REQ-ADAPTER-ADD-SURFACE-ERRORS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7472976Z   [must] requirement_quality REQ-ADAPTER-ADD-SURFACE-ERRORS criterion=length — title is 77 words; want 3..=25
2026-09-06T13:09:01.7473711Z   [must] requirement_quality REQ-ADAPTER-FLOOR-ENFORCE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7474423Z   [must] requirement_quality REQ-ADAPTER-FLOOR-ENFORCE criterion=length — title is 419 words; want 3..=25
2026-09-06T13:09:01.7475156Z   [must] requirement_quality REQ-ADAPTER-FLOOR-VS-STAGED-CORE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7475910Z   [must] requirement_quality REQ-ADAPTER-FLOOR-VS-STAGED-CORE criterion=length — title is 1130 words; want 3..=25
2026-09-06T13:09:01.7476645Z   [must] requirement_quality REQ-ADAPTER-GH-TRANSPORT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7477337Z   [must] requirement_quality REQ-ADAPTER-GH-TRANSPORT criterion=length — title is 62 words; want 3..=25
2026-09-06T13:09:01.7478037Z   [must] requirement_quality REQ-ADAPTER-LIVE-UPDATE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7478728Z   [must] requirement_quality REQ-ADAPTER-LIVE-UPDATE criterion=length — title is 168 words; want 3..=25
2026-09-06T13:09:01.7479440Z   [must] requirement_quality REQ-ADAPTER-MULTIPLATFORM-SPT criterion=length — title is 123 words; want 3..=25
2026-09-06T13:09:01.7480084Z   [must] requirement_quality REQ-ADAPTER-PROOF-DIR-OVERRIDE criterion=length — title is 76 words; want 3..=25
2026-09-06T13:09:01.7480862Z   [must] requirement_quality REQ-ADAPTER-TEMPLATE-KEY-VALIDATION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7481758Z   [must] requirement_quality REQ-ADAPTER-TEMPLATE-KEY-VALIDATION criterion=length — title is 110 words; want 3..=25
2026-09-06T13:09:01.7482506Z   [must] requirement_quality REQ-ADAPTER-TRANSLATE-PROOF criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7483302Z   [must] requirement_quality REQ-ADAPTER-TRANSLATE-PROOF criterion=length — title is 182 words; want 3..=25
2026-09-06T13:09:01.7484063Z   [must] requirement_quality REQ-ADAPTER-UNRESOLVED-HINT-FORM criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7484810Z   [must] requirement_quality REQ-ADAPTER-UNRESOLVED-HINT-FORM criterion=length — title is 116 words; want 3..=25
2026-09-06T13:09:01.7485549Z   [must] requirement_quality REQ-ADAPTER-UPDATE-INPLACE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7486251Z   [must] requirement_quality REQ-ADAPTER-UPDATE-INPLACE criterion=length — title is 82 words; want 3..=25
2026-09-06T13:09:01.7486883Z   [must] requirement_quality REQ-ADAPTER-UPDATE-MESSAGE criterion=length — title is 64 words; want 3..=25
2026-09-06T13:09:01.7487482Z   [must] requirement_quality REQ-ADAPTER-UPDATE-POST criterion=length — title is 124 words; want 3..=25
2026-09-06T13:09:01.7488179Z   [must] requirement_quality REQ-ADAPTER-VERSION-CMD criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7488916Z   [must] requirement_quality REQ-ADAPTER-VERSION-CMD criterion=length — title is 59 words; want 3..=25
2026-09-06T13:09:01.7489653Z   [must] requirement_quality REQ-API-1 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7490334Z   [must] requirement_quality REQ-API-4 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7490911Z   [must] requirement_quality REQ-API-4 criterion=length — title is 67 words; want 3..=25
2026-09-06T13:09:01.7491460Z   [must] requirement_quality REQ-API-ENDPOINT-INFO criterion=length — title is 138 words; want 3..=25
2026-09-06T13:09:01.7492136Z   [must] requirement_quality REQ-ATTACH-AWAY-ALERTS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7492822Z   [must] requirement_quality REQ-ATTACH-AWAY-ALERTS criterion=length — title is 372 words; want 3..=25
2026-09-06T13:09:01.7493438Z   [must] requirement_quality REQ-ATTACH-IDEMPOTENT-REPLAY criterion=length — title is 257 words; want 3..=25
2026-09-06T13:09:01.7494143Z   [must] requirement_quality REQ-ATTACH-LINK-PUSH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7494807Z   [must] requirement_quality REQ-ATTACH-LINK-PUSH criterion=length — title is 364 words; want 3..=25
2026-09-06T13:09:01.7495500Z   [must] requirement_quality REQ-ATTACH-RESIZE-REPAINT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7496205Z   [must] requirement_quality REQ-ATTACH-RESIZE-REPAINT criterion=length — title is 328 words; want 3..=25
2026-09-06T13:09:01.7497030Z   [must] requirement_quality REQ-ATTACH-SEED-REQUEST-DOUBLE-SERVE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7497805Z   [must] requirement_quality REQ-ATTACH-SEED-REQUEST-DOUBLE-SERVE criterion=length — title is 310 words; want 3..=25
2026-09-06T13:09:01.7498538Z   [must] requirement_quality REQ-BENCH-STEP-LEDGER criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7499296Z   [must] requirement_quality REQ-BENCH-STEP-LEDGER criterion=length — title is 336 words; want 3..=25
2026-09-06T13:09:01.7500405Z   [must] requirement_quality REQ-BIND-HONEST-SELF-STAMP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7501128Z   [must] requirement_quality REQ-BIND-HONEST-SELF-STAMP criterion=length — title is 182 words; want 3..=25
2026-09-06T13:09:01.7502029Z   [must] requirement_quality REQ-BIND-PSYCHE-CUSTODY-SQUAT-GUARD criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7502805Z   [must] requirement_quality REQ-BIND-PSYCHE-CUSTODY-SQUAT-GUARD criterion=length — title is 134 words; want 3..=25
2026-09-06T13:09:01.7503636Z   [must] requirement_quality REQ-BOUNDARY-ROTATION-CREDENTIAL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7504393Z   [must] requirement_quality REQ-BOUNDARY-ROTATION-CREDENTIAL criterion=length — title is 53 words; want 3..=25
2026-09-06T13:09:01.7505012Z   [must] requirement_quality REQ-BRAIN-HASH-ONCE criterion=length — title is 127 words; want 3..=25
2026-09-06T13:09:01.7505730Z   [must] requirement_quality REQ-BRAIN-READY-WINDOW-OBSERVABLE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7506498Z   [must] requirement_quality REQ-BRAIN-READY-WINDOW-OBSERVABLE criterion=length — title is 363 words; want 3..=25
2026-09-06T13:09:01.7507251Z   [must] requirement_quality REQ-BRAIN-RESUME-NO-CONN-DEADLOCK criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7507990Z   [must] requirement_quality REQ-BRAIN-RESUME-NO-CONN-DEADLOCK criterion=length — title is 477 words; want 3..=25
2026-09-06T13:09:01.7508812Z   [must] requirement_quality REQ-BRAIN-RESUME-NO-CONTROL-STEAL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7509680Z   [must] requirement_quality REQ-BRAIN-RESUME-NO-CONTROL-STEAL criterion=length — title is 498 words; want 3..=25
2026-09-06T13:09:01.7510437Z   [must] requirement_quality REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7511208Z   [must] requirement_quality REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE criterion=length — title is 282 words; want 3..=25
2026-09-06T13:09:01.7511989Z   [must] requirement_quality REQ-BROKER-ATTACH-JOURNAL-RESILIENT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7512758Z   [must] requirement_quality REQ-BROKER-ATTACH-JOURNAL-RESILIENT criterion=length — title is 120 words; want 3..=25
2026-09-06T13:09:01.7513505Z   [must] requirement_quality REQ-BROKER-EXIT-EVERY-REAP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7514215Z   [must] requirement_quality REQ-BROKER-EXIT-EVERY-REAP criterion=length — title is 381 words; want 3..=25
2026-09-06T13:09:01.7514930Z   [must] requirement_quality REQ-BROKER-OUTPUT-BEFORE-EXIT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7515649Z   [must] requirement_quality REQ-BROKER-OUTPUT-BEFORE-EXIT criterion=length — title is 133 words; want 3..=25
2026-09-06T13:09:01.7516324Z   [must] requirement_quality REQ-BROKER-OUTPUT-BEFORE-EXIT criterion=tbd-todo — title contains placeholder marker 'XXX'
2026-09-06T13:09:01.7517058Z   [must] requirement_quality REQ-BROKER-SCREEN-GRID criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7517735Z   [must] requirement_quality REQ-BROKER-SCREEN-GRID criterion=length — title is 126 words; want 3..=25
2026-09-06T13:09:01.7518525Z   [must] requirement_quality REQ-BROKER-STOP-DENY-NAMES-BLAST criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7519343Z   [must] requirement_quality REQ-BROKER-STOP-DENY-NAMES-BLAST criterion=length — title is 364 words; want 3..=25
2026-09-06T13:09:01.7520078Z   [must] requirement_quality REQ-BROKER-STOP-ENDPOINT-DENY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7520799Z   [must] requirement_quality REQ-BROKER-STOP-ENDPOINT-DENY criterion=length — title is 485 words; want 3..=25
2026-09-06T13:09:01.7521633Z   [must] requirement_quality REQ-BUILD-UNIQUE-BIN-NAMES criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7522339Z   [must] requirement_quality REQ-BUILD-UNIQUE-BIN-NAMES criterion=length — title is 124 words; want 3..=25
2026-09-06T13:09:01.7523055Z   [must] requirement_quality REQ-CARRIER-CLAIM-EXCLUSIVE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7523770Z   [must] requirement_quality REQ-CARRIER-CLAIM-EXCLUSIVE criterion=length — title is 218 words; want 3..=25
2026-09-06T13:09:01.7524465Z   [must] requirement_quality REQ-CI-DOCS-ONLY-THIN criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7525132Z   [must] requirement_quality REQ-CI-DOCS-ONLY-THIN criterion=length — title is 176 words; want 3..=25
2026-09-06T13:09:01.7525824Z   [must] requirement_quality REQ-CI-FREE-SPACE-PREFLIGHT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7526540Z   [must] requirement_quality REQ-CI-FREE-SPACE-PREFLIGHT criterion=length — title is 653 words; want 3..=25
2026-09-06T13:09:01.7527256Z   [must] requirement_quality REQ-CI-LINK-HEALTH-PROBE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7527999Z   [must] requirement_quality REQ-CI-LINK-HEALTH-PROBE criterion=length — title is 444 words; want 3..=25
2026-09-06T13:09:01.7528715Z   [must] requirement_quality REQ-CI-LOAD-BEARING-PATCH-PIN criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7529523Z   [must] requirement_quality REQ-CI-LOAD-BEARING-PATCH-PIN criterion=length — title is 469 words; want 3..=25
2026-09-06T13:09:01.7530237Z   [must] requirement_quality REQ-CI-LOCAL-GATE-DISPOSAL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7530943Z   [must] requirement_quality REQ-CI-LOCAL-GATE-DISPOSAL criterion=length — title is 124 words; want 3..=25
2026-09-06T13:09:01.7531662Z   [must] requirement_quality REQ-CI-NOTIFY-CANCELLED-CLASS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7532376Z   [must] requirement_quality REQ-CI-NOTIFY-CANCELLED-CLASS criterion=length — title is 269 words; want 3..=25
2026-09-06T13:09:01.7533095Z   [must] requirement_quality REQ-CI-POSTJOB-DAEMON-REAP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7533796Z   [must] requirement_quality REQ-CI-POSTJOB-DAEMON-REAP criterion=length — title is 461 words; want 3..=25
2026-09-06T13:09:01.7534529Z   [must] requirement_quality REQ-CI-PRERELINK-EXE-LOCK-REAP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7535255Z   [must] requirement_quality REQ-CI-PRERELINK-EXE-LOCK-REAP criterion=length — title is 436 words; want 3..=25
2026-09-06T13:09:01.7536003Z   [must] requirement_quality REQ-CI-TOOLCHAIN-VERSION-PRINT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7536726Z   [must] requirement_quality REQ-CI-TOOLCHAIN-VERSION-PRINT criterion=length — title is 395 words; want 3..=25
2026-09-06T13:09:01.7537516Z   [must] requirement_quality REQ-CI-WINDOWS-PHASE-A-BOUND criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7538222Z   [must] requirement_quality REQ-CI-WINDOWS-PHASE-A-BOUND criterion=length — title is 466 words; want 3..=25
2026-09-06T13:09:01.7538789Z   [must] requirement_quality REQ-CLI-1 criterion=length — title is 97 words; want 3..=25
2026-09-06T13:09:01.7539414Z   [must] requirement_quality REQ-CLI-2 criterion=length — title is 37 words; want 3..=25
2026-09-06T13:09:01.7539910Z   [must] requirement_quality REQ-CLI-3 criterion=length — title is 37 words; want 3..=25
2026-09-06T13:09:01.7540535Z   [must] requirement_quality REQ-CLI-4 criterion=length — title is 89 words; want 3..=25
2026-09-06T13:09:01.7541098Z   [must] requirement_quality REQ-CLI-BROKEN-PIPE-TOLERANT criterion=length — title is 78 words; want 3..=25
2026-09-06T13:09:01.7541832Z   [must] requirement_quality REQ-CLI-HELP-MARKDOWN criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7542511Z   [must] requirement_quality REQ-CLI-HELP-MARKDOWN criterion=length — title is 156 words; want 3..=25
2026-09-06T13:09:01.7543068Z   [must] requirement_quality REQ-CLI-JSON criterion=length — title is 95 words; want 3..=25
2026-09-06T13:09:01.7543750Z   [must] requirement_quality REQ-CLI-MONIC-TRIGGER-SECTION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7544472Z   [must] requirement_quality REQ-CLI-MONIC-TRIGGER-SECTION criterion=length — title is 399 words; want 3..=25
2026-09-06T13:09:01.7545186Z   [must] requirement_quality REQ-CLI-NODE-VERB-PRIMARY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7545891Z   [must] requirement_quality REQ-CLI-NODE-VERB-PRIMARY criterion=length — title is 540 words; want 3..=25
2026-09-06T13:09:01.7546768Z   [must] requirement_quality REQ-CLI-OUTPUT-MARKDOWN criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7562114Z   [must] requirement_quality REQ-CLI-OUTPUT-MARKDOWN criterion=length — title is 199 words; want 3..=25
2026-09-06T13:09:01.7562933Z   [must] requirement_quality REQ-CLI-STACK-HEADROOM criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7563626Z   [must] requirement_quality REQ-CLI-STACK-HEADROOM criterion=length — title is 384 words; want 3..=25
2026-09-06T13:09:01.7564343Z   [must] requirement_quality REQ-CLI-SURFACE-SECTION-SITED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7565086Z   [must] requirement_quality REQ-CLI-SURFACE-SECTION-SITED criterion=length — title is 368 words; want 3..=25
2026-09-06T13:09:01.7565801Z   [must] requirement_quality REQ-CLI-WIN-VT-ENABLE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7566457Z   [must] requirement_quality REQ-CLI-WIN-VT-ENABLE criterion=length — title is 110 words; want 3..=25
2026-09-06T13:09:01.7567188Z   [must] requirement_quality REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7567964Z   [must] requirement_quality REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS criterion=length — title is 147 words; want 3..=25
2026-09-06T13:09:01.7568699Z   [must] requirement_quality REQ-CONN-POISON-ATTRIBUTION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7569497Z   [must] requirement_quality REQ-CONN-POISON-ATTRIBUTION criterion=length — title is 227 words; want 3..=25
2026-09-06T13:09:01.7570202Z   [must] requirement_quality REQ-CONN-POISON-DIAL-SCOPE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7570894Z   [must] requirement_quality REQ-CONN-POISON-DIAL-SCOPE criterion=length — title is 245 words; want 3..=25
2026-09-06T13:09:01.7571562Z   [must] requirement_quality REQ-CONSENT-1 criterion=length — title is 41 words; want 3..=25
2026-09-06T13:09:01.7572078Z   [must] requirement_quality REQ-CONSENT-2 criterion=length — title is 37 words; want 3..=25
2026-09-06T13:09:01.7572583Z   [must] requirement_quality REQ-CONSENT-3 criterion=length — title is 82 words; want 3..=25
2026-09-06T13:09:01.7573150Z   [must] requirement_quality REQ-CONTROL-STAMP-CONVERGENCE criterion=length — title is 82 words; want 3..=25
2026-09-06T13:09:01.7573874Z   [must] requirement_quality REQ-CONTROLLER-LEASE-IDENTITY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7574730Z   [must] requirement_quality REQ-CONTROLLER-LEASE-IDENTITY criterion=length — title is 374 words; want 3..=25
2026-09-06T13:09:01.7575450Z   [must] requirement_quality REQ-CONTROLLER-LIVENESS-REAP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7576167Z   [must] requirement_quality REQ-CONTROLLER-LIVENESS-REAP criterion=length — title is 370 words; want 3..=25
2026-09-06T13:09:01.7576895Z   [must] requirement_quality REQ-CONV-1 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7577499Z   [must] requirement_quality REQ-CONV-1 criterion=length — title is 73 words; want 3..=25
2026-09-06T13:09:01.7577986Z   [must] requirement_quality REQ-CONV-2 criterion=length — title is 47 words; want 3..=25
2026-09-06T13:09:01.7578534Z   [must] requirement_quality REQ-CRC-SWAP-OLD-DISPLACE criterion=length — title is 125 words; want 3..=25
2026-09-06T13:09:01.7579346Z   [must] requirement_quality REQ-CREATE-BIND-REST-ACTIVE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7580052Z   [must] requirement_quality REQ-CREATE-BIND-REST-ACTIVE criterion=length — title is 249 words; want 3..=25
2026-09-06T13:09:01.7580710Z   [must] requirement_quality REQ-DAEMON-5 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7581311Z   [must] requirement_quality REQ-DAEMON-5 criterion=length — title is 64 words; want 3..=25
2026-09-06T13:09:01.7581911Z   [must] requirement_quality REQ-DAEMON-6 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7582499Z   [must] requirement_quality REQ-DAEMON-6 criterion=length — title is 84 words; want 3..=25
2026-09-06T13:09:01.7583090Z   [must] requirement_quality REQ-DAEMON-7 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7583687Z   [must] requirement_quality REQ-DAEMON-7 criterion=length — title is 62 words; want 3..=25
2026-09-06T13:09:01.7584188Z   [must] requirement_quality REQ-DAEMON-8 criterion=length — title is 44 words; want 3..=25
2026-09-06T13:09:01.7584785Z   [must] requirement_quality REQ-DAEMON-9 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7585376Z   [must] requirement_quality REQ-DAEMON-9 criterion=length — title is 114 words; want 3..=25
2026-09-06T13:09:01.7586039Z   [must] requirement_quality REQ-DAEMON-BITS-AMBIGUITY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7586727Z   [must] requirement_quality REQ-DAEMON-BITS-AMBIGUITY criterion=length — title is 905 words; want 3..=25
2026-09-06T13:09:01.7587402Z   [must] requirement_quality REQ-DAEMON-REFRESH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7588037Z   [must] requirement_quality REQ-DAEMON-REFRESH criterion=length — title is 147 words; want 3..=25
2026-09-06T13:09:01.7588765Z   [must] requirement_quality REQ-DAEMON-RESTART-SELF-INTERRUPTION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7589627Z   [must] requirement_quality REQ-DAEMON-RESTART-SELF-INTERRUPTION criterion=length — title is 312 words; want 3..=25
2026-09-06T13:09:01.7590482Z   [must] requirement_quality REQ-DAEMON-SERVICE-INSTALL criterion=length — title is 88 words; want 3..=25
2026-09-06T13:09:01.7591195Z   [must] requirement_quality REQ-DAEMON-STATUS-JSON-TRUTH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7591895Z   [must] requirement_quality REQ-DAEMON-STATUS-JSON-TRUTH criterion=length — title is 73 words; want 3..=25
2026-09-06T13:09:01.7592499Z   [must] requirement_quality REQ-DAEMON-STDERR-PERSIST criterion=length — title is 55 words; want 3..=25
2026-09-06T13:09:01.7593122Z   [must] requirement_quality REQ-DAEMON-STOP-LIVE-SESSION-WARN criterion=length — title is 28 words; want 3..=25
2026-09-06T13:09:01.7593984Z   [must] requirement_quality REQ-DIAL-FAILED-ADDR-MEMORY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7594682Z   [must] requirement_quality REQ-DIAL-FAILED-ADDR-MEMORY criterion=length — title is 213 words; want 3..=25
2026-09-06T13:09:01.7595403Z   [must] requirement_quality REQ-DIAL-ROUTE-LEG-ATTRIBUTION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7596285Z   [must] requirement_quality REQ-DIAL-ROUTE-LEG-ATTRIBUTION criterion=length — title is 275 words; want 3..=25
2026-09-06T13:09:01.7597076Z   [must] requirement_quality REQ-DIGEST-CROSS-NODE-PULL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7597771Z   [must] requirement_quality REQ-DIGEST-CROSS-NODE-PULL criterion=length — title is 127 words; want 3..=25
2026-09-06T13:09:01.7598433Z   [must] requirement_quality REQ-DIGEST-CURSOR criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7599202Z   [must] requirement_quality REQ-DIGEST-CURSOR criterion=length — title is 181 words; want 3..=25
2026-09-06T13:09:01.7599992Z   [must] requirement_quality REQ-DIGEST-FETCHER-STRATEGY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7600700Z   [must] requirement_quality REQ-DIGEST-FETCHER-STRATEGY criterion=length — title is 167 words; want 3..=25
2026-09-06T13:09:01.7601435Z   [must] requirement_quality REQ-DIGEST-GENERATION-SUPERSEDE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7602164Z   [must] requirement_quality REQ-DIGEST-GENERATION-SUPERSEDE criterion=length — title is 353 words; want 3..=25
2026-09-06T13:09:01.7602893Z   [must] requirement_quality REQ-DIGEST-JSON-SELF-CONTAINED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7603619Z   [must] requirement_quality REQ-DIGEST-JSON-SELF-CONTAINED criterion=length — title is 404 words; want 3..=25
2026-09-06T13:09:01.7604312Z   [must] requirement_quality REQ-DIGEST-PROFILE-ENV criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7604970Z   [must] requirement_quality REQ-DIGEST-PROFILE-ENV criterion=length — title is 96 words; want 3..=25
2026-09-06T13:09:01.7605673Z   [must] requirement_quality REQ-DIGEST-SEAL-ON-IDLE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7606339Z   [must] requirement_quality REQ-DIGEST-SEAL-ON-IDLE criterion=length — title is 166 words; want 3..=25
2026-09-06T13:09:01.7607021Z   [must] requirement_quality REQ-DISK-FLOOR-PREFLIGHT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7608128Z   [must] requirement_quality REQ-DISK-FLOOR-PREFLIGHT criterion=length — title is 328 words; want 3..=25
2026-09-06T13:09:01.7608818Z   [must] requirement_quality REQ-DISPATCH-CLAIM-RETRY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7609566Z   [must] requirement_quality REQ-DISPATCH-CLAIM-RETRY criterion=length — title is 117 words; want 3..=25
2026-09-06T13:09:01.7610305Z   [must] requirement_quality REQ-DISPATCH-FALLBACK-CIRCUIT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7611103Z   [must] requirement_quality REQ-DISPATCH-FALLBACK-CIRCUIT criterion=length — title is 160 words; want 3..=25
2026-09-06T13:09:01.7611733Z   [must] requirement_quality REQ-DISPATCH-HYGIENE-TELEMETRY criterion=length — title is 126 words; want 3..=25
2026-09-06T13:09:01.7612334Z   [must] requirement_quality REQ-DOC-DELIVERY-VOCAB criterion=length — title is 52 words; want 3..=25
2026-09-06T13:09:01.7612926Z   [must] requirement_quality REQ-DOC-ECHO-COMMUNE-CONTRACT criterion=length — title is 60 words; want 3..=25
2026-09-06T13:09:01.7613980Z   [must] requirement_quality REQ-DOC-ENDPOINT-DROP-RESOLUTION criterion=length — title is 57 words; want 3..=25
2026-09-06T13:09:01.7614577Z   [must] requirement_quality REQ-DOCS-LOCAL-SERVER criterion=length — title is 181 words; want 3..=25
2026-09-06T13:09:01.7615286Z   [must] requirement_quality REQ-DOCS-NO-INTERIOR-SPACE-RUNS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7616076Z   [must] requirement_quality REQ-DOCS-NO-INTERIOR-SPACE-RUNS criterion=length — title is 359 words; want 3..=25
2026-09-06T13:09:01.7616814Z   [must] requirement_quality REQ-DOCS-NO-INTERNAL-CODES criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7617543Z   [must] requirement_quality REQ-DOCS-NO-INTERNAL-CODES criterion=length — title is 84 words; want 3..=25
2026-09-06T13:09:01.7618139Z   [must] requirement_quality REQ-DOCS-RELEASE-ASSET criterion=length — title is 120 words; want 3..=25
2026-09-06T13:09:01.7618868Z   [must] requirement_quality REQ-DRIVEN-BY-OWN-NODE-NORMALIZE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7619679Z   [must] requirement_quality REQ-DRIVEN-BY-OWN-NODE-NORMALIZE criterion=length — title is 463 words; want 3..=25
2026-09-06T13:09:01.7620370Z   [must] requirement_quality REQ-DSR-SINGLE-CPR criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7621023Z   [must] requirement_quality REQ-DSR-SINGLE-CPR criterion=length — title is 91 words; want 3..=25
2026-09-06T13:09:01.7621763Z   [must] requirement_quality REQ-ECHO-BOUNDARY-INPUT-BEFORE-ROTATION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7622560Z   [must] requirement_quality REQ-ECHO-BOUNDARY-INPUT-BEFORE-ROTATION criterion=length — title is 291 words; want 3..=25
2026-09-06T13:09:01.7623317Z   [must] requirement_quality REQ-ECHO-BRIEF-DIRECT-ROUTE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7624032Z   [must] requirement_quality REQ-ECHO-BRIEF-DIRECT-ROUTE criterion=length — title is 353 words; want 3..=25
2026-09-06T13:09:01.7624639Z   [must] requirement_quality REQ-ECHO-DROP-DIR-RESOLVE criterion=length — title is 127 words; want 3..=25
2026-09-06T13:09:01.7625335Z   [must] requirement_quality REQ-ECHO-IDLE-AGE-GATE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7625999Z   [must] requirement_quality REQ-ECHO-IDLE-AGE-GATE criterion=length — title is 337 words; want 3..=25
2026-09-06T13:09:01.7626703Z   [must] requirement_quality REQ-EFFECTIVE-INSTANCE-STATE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7627412Z   [must] requirement_quality REQ-EFFECTIVE-INSTANCE-STATE criterion=length — title is 160 words; want 3..=25
2026-09-06T13:09:01.7628084Z   [must] requirement_quality REQ-ELEVATE-1 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7628704Z   [must] requirement_quality REQ-ELEVATE-1 criterion=length — title is 121 words; want 3..=25
2026-09-06T13:09:01.7629352Z   [must] requirement_quality REQ-ENDPOINT-AUTOSTART criterion=length — title is 249 words; want 3..=25
2026-09-06T13:09:01.7630119Z   [must] requirement_quality REQ-ENDPOINT-CYCLE-HONEST criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7630810Z   [must] requirement_quality REQ-ENDPOINT-CYCLE-HONEST criterion=length — title is 104 words; want 3..=25
2026-09-06T13:09:01.7631527Z   [must] requirement_quality REQ-ENDPOINT-DROP-DIR-SURFACE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7632243Z   [must] requirement_quality REQ-ENDPOINT-DROP-DIR-SURFACE criterion=length — title is 394 words; want 3..=25
2026-09-06T13:09:01.7633005Z   [must] requirement_quality REQ-ENDPOINT-LIST-JOINED-SUBNETS-LABEL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7633959Z   [must] requirement_quality REQ-ENDPOINT-LIST-JOINED-SUBNETS-LABEL criterion=length — title is 278 words; want 3..=25
2026-09-06T13:09:01.7634611Z   [must] requirement_quality REQ-ENDPOINT-LIST-MERGE-LOCAL criterion=length — title is 95 words; want 3..=25
2026-09-06T13:09:01.7635396Z   [must] requirement_quality REQ-ENDPOINT-LIST-NODE-GROUPED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7636120Z   [must] requirement_quality REQ-ENDPOINT-LIST-NODE-GROUPED criterion=length — title is 237 words; want 3..=25
2026-09-06T13:09:01.7636745Z   [must] requirement_quality REQ-ENDPOINT-LIST-NODE-IDENT criterion=length — title is 57 words; want 3..=25
2026-09-06T13:09:01.7637485Z   [must] requirement_quality REQ-ENDPOINT-LIST-OWN-NESTED-SECTION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7638259Z   [must] requirement_quality REQ-ENDPOINT-LIST-OWN-NESTED-SECTION criterion=length — title is 384 words; want 3..=25
2026-09-06T13:09:01.7639078Z   [must] requirement_quality REQ-ENDPOINT-LIST-PALETTE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7639765Z   [must] requirement_quality REQ-ENDPOINT-LIST-PALETTE criterion=length — title is 106 words; want 3..=25
2026-09-06T13:09:01.7640388Z   [must] requirement_quality REQ-ENDPOINT-LIST-PROJECT-COL criterion=length — title is 79 words; want 3..=25
2026-09-06T13:09:01.7641017Z   [must] requirement_quality REQ-ENDPOINT-LIST-RENDER-POLISH criterion=length — title is 122 words; want 3..=25
2026-09-06T13:09:01.7641652Z   [must] requirement_quality REQ-ENDPOINT-LIST-REST-FILTER criterion=length — title is 110 words; want 3..=25
2026-09-06T13:09:01.7642416Z   [must] requirement_quality REQ-ENDPOINT-MESSAGE-ONLY-DISPLAY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7643193Z   [must] requirement_quality REQ-ENDPOINT-MESSAGE-ONLY-DISPLAY criterion=length — title is 243 words; want 3..=25
2026-09-06T13:09:01.7643813Z   [must] requirement_quality REQ-ENDPOINT-ONLINE-TRUTH criterion=length — title is 145 words; want 3..=25
2026-09-06T13:09:01.7644491Z   [must] requirement_quality REQ-ENDPOINT-PURGE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7645142Z   [must] requirement_quality REQ-ENDPOINT-PURGE criterion=length — title is 220 words; want 3..=25
2026-09-06T13:09:01.7645722Z   [must] requirement_quality REQ-ENDPOINT-STOP-OFFLINE criterion=length — title is 58 words; want 3..=25
2026-09-06T13:09:01.7646441Z   [must] requirement_quality REQ-ENDPOINT-STOP-RESOLVES criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7647148Z   [must] requirement_quality REQ-ENDPOINT-STOP-RESOLVES criterion=length — title is 262 words; want 3..=25
2026-09-06T13:09:01.7647876Z   [must] requirement_quality REQ-ENDPOINT-TEARDOWN-AUTHORITY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7648609Z   [must] requirement_quality REQ-ENDPOINT-TEARDOWN-AUTHORITY criterion=length — title is 539 words; want 3..=25
2026-09-06T13:09:01.7649420Z   [must] requirement_quality REQ-ENDPOINT-UNBOUND-ATTACH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7650191Z   [must] requirement_quality REQ-ENDPOINT-UNBOUND-ATTACH criterion=length — title is 122 words; want 3..=25
2026-09-06T13:09:01.7650939Z   [must] requirement_quality REQ-ENGINEROOM-ADMIN-BRINGUP-EMPOWERS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7651712Z   [must] requirement_quality REQ-ENGINEROOM-ADMIN-BRINGUP-EMPOWERS criterion=length — title is 391 words; want 3..=25
2026-09-06T13:09:01.7652469Z   [must] requirement_quality REQ-ENSURE-DAEMON-STOP-INHIBIT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7653291Z   [must] requirement_quality REQ-ENSURE-DAEMON-STOP-INHIBIT criterion=length — title is 252 words; want 3..=25
2026-09-06T13:09:01.7653945Z   [must] requirement_quality REQ-EP-6 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7654570Z   [must] requirement_quality REQ-EP-6 criterion=length — title is 58 words; want 3..=25
2026-09-06T13:09:01.7655051Z   [must] requirement_quality REQ-EP-7 criterion=length — title is 68 words; want 3..=25
2026-09-06T13:09:01.7655524Z   [must] requirement_quality REQ-EP-8 criterion=length — title is 114 words; want 3..=25
2026-09-06T13:09:01.7656106Z   [must] requirement_quality REQ-EP-9 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7656668Z   [must] requirement_quality REQ-EP-9 criterion=length — title is 76 words; want 3..=25
2026-09-06T13:09:01.7657321Z   [must] requirement_quality REQ-ER-BRIEFING-PRESENTED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7658009Z   [must] requirement_quality REQ-ER-BRIEFING-PRESENTED criterion=length — title is 655 words; want 3..=25
2026-09-06T13:09:01.7658716Z   [must] requirement_quality REQ-ER-BRIEFING-SESSION-SCOPED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7659531Z   [must] requirement_quality REQ-ER-BRIEFING-SESSION-SCOPED criterion=length — title is 482 words; want 3..=25
2026-09-06T13:09:01.7660251Z   [must] requirement_quality REQ-ER-BRIEFING-SURFACE-VOCAB criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7660967Z   [must] requirement_quality REQ-ER-BRIEFING-SURFACE-VOCAB criterion=length — title is 340 words; want 3..=25
2026-09-06T13:09:01.7661715Z   [must] requirement_quality REQ-ER-BRINGUP-ABSENT-CODE-UNCOUNTED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7662553Z   [must] requirement_quality REQ-ER-BRINGUP-ABSENT-CODE-UNCOUNTED criterion=length — title is 384 words; want 3..=25
2026-09-06T13:09:01.7663301Z   [must] requirement_quality REQ-ER-BRINGUP-ANCHORS-PERCH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7664013Z   [must] requirement_quality REQ-ER-BRINGUP-ANCHORS-PERCH criterion=length — title is 604 words; want 3..=25
2026-09-06T13:09:01.7664728Z   [must] requirement_quality REQ-ER-BRINGUP-ATTEMPT-BOUND criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7665426Z   [must] requirement_quality REQ-ER-BRINGUP-ATTEMPT-BOUND criterion=length — title is 320 words; want 3..=25
2026-09-06T13:09:01.7666145Z   [must] requirement_quality REQ-ER-BRINGUP-INFLIGHT-LEDGER criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7666866Z   [must] requirement_quality REQ-ER-BRINGUP-INFLIGHT-LEDGER criterion=length — title is 432 words; want 3..=25
2026-09-06T13:09:01.7667605Z   [must] requirement_quality REQ-ER-BRINGUP-LAUNCH-PHASE-TRACE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7668341Z   [must] requirement_quality REQ-ER-BRINGUP-LAUNCH-PHASE-TRACE criterion=length — title is 392 words; want 3..=25
2026-09-06T13:09:01.7669208Z   [must] requirement_quality REQ-ER-BRINGUP-PROMPT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7669871Z   [must] requirement_quality REQ-ER-BRINGUP-PROMPT criterion=length — title is 174 words; want 3..=25
2026-09-06T13:09:01.7670573Z   [must] requirement_quality REQ-ER-BRINGUP-PROMPT-NAMES-SEAT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7671302Z   [must] requirement_quality REQ-ER-BRINGUP-PROMPT-NAMES-SEAT criterion=length — title is 318 words; want 3..=25
2026-09-06T13:09:01.7672169Z   [must] requirement_quality REQ-ER-BRINGUP-SPAWNS-SESSION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7672889Z   [must] requirement_quality REQ-ER-BRINGUP-SPAWNS-SESSION criterion=length — title is 1377 words; want 3..=25
2026-09-06T13:09:01.7673609Z   [must] requirement_quality REQ-ER-BRINGUP-TOTP-GATE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7674339Z   [must] requirement_quality REQ-ER-BRINGUP-TOTP-GATE criterion=length — title is 200 words; want 3..=25
2026-09-06T13:09:01.7675017Z   [must] requirement_quality REQ-ER-CEREMONY-VERB criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7675680Z   [must] requirement_quality REQ-ER-CEREMONY-VERB criterion=length — title is 237 words; want 3..=25
2026-09-06T13:09:01.7676404Z   [must] requirement_quality REQ-ER-CONTROLLER-BOUND-POSTURE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7677173Z   [must] requirement_quality REQ-ER-CONTROLLER-BOUND-POSTURE criterion=length — title is 197 words; want 3..=25
2026-09-06T13:09:01.7677918Z   [must] requirement_quality REQ-ER-GRANT-ANNOUNCE-AFTER-ATTACH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7678675Z   [must] requirement_quality REQ-ER-GRANT-ANNOUNCE-AFTER-ATTACH criterion=length — title is 349 words; want 3..=25
2026-09-06T13:09:01.7679466Z   [must] requirement_quality REQ-ER-INBOUND-LOCK criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7680117Z   [must] requirement_quality REQ-ER-INBOUND-LOCK criterion=length — title is 186 words; want 3..=25
2026-09-06T13:09:01.7680805Z   [must] requirement_quality REQ-ER-INBOUND-LOCK-ALL-PATHS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7681519Z   [must] requirement_quality REQ-ER-INBOUND-LOCK-ALL-PATHS criterion=length — title is 993 words; want 3..=25
2026-09-06T13:09:01.7682205Z   [must] requirement_quality REQ-ER-NOT-ADVERTISED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7682867Z   [must] requirement_quality REQ-ER-NOT-ADVERTISED criterion=length — title is 178 words; want 3..=25
2026-09-06T13:09:01.7683587Z   [must] requirement_quality REQ-ER-OFFLINE-BRINGUP-REACHABLE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7684318Z   [must] requirement_quality REQ-ER-OFFLINE-BRINGUP-REACHABLE criterion=length — title is 500 words; want 3..=25
2026-09-06T13:09:01.7685015Z   [must] requirement_quality REQ-ER-PURGE-RESETS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7685668Z   [must] requirement_quality REQ-ER-PURGE-RESETS criterion=length — title is 181 words; want 3..=25
2026-09-06T13:09:01.7686340Z   [must] requirement_quality REQ-ER-RC-INTENT-LOCKS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7687002Z   [must] requirement_quality REQ-ER-RC-INTENT-LOCKS criterion=length — title is 210 words; want 3..=25
2026-09-06T13:09:01.7687686Z   [must] requirement_quality REQ-ER-RESERVED-ENDPOINT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7688440Z   [must] requirement_quality REQ-ER-RESERVED-ENDPOINT criterion=length — title is 190 words; want 3..=25
2026-09-06T13:09:01.7689235Z   [must] requirement_quality REQ-ER-RESERVED-ID-SPAWN-REFUSAL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7689978Z   [must] requirement_quality REQ-ER-RESERVED-ID-SPAWN-REFUSAL criterion=length — title is 500 words; want 3..=25
2026-09-06T13:09:01.7690711Z   [must] requirement_quality REQ-ER-ROLE-STATIC-IMMUTABLE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7691530Z   [must] requirement_quality REQ-ER-ROLE-STATIC-IMMUTABLE criterion=length — title is 420 words; want 3..=25
2026-09-06T13:09:01.7692241Z   [must] requirement_quality REQ-ER-RULESET-NODE-NAMES criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7692984Z   [must] requirement_quality REQ-ER-RULESET-NODE-NAMES criterion=length — title is 309 words; want 3..=25
2026-09-06T13:09:01.7693666Z   [must] requirement_quality REQ-ER-RULESET-TABLE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7694319Z   [must] requirement_quality REQ-ER-RULESET-TABLE criterion=length — title is 310 words; want 3..=25
2026-09-06T13:09:01.7694991Z   [must] requirement_quality REQ-ER-SEQUESTERED-CWD criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7695670Z   [must] requirement_quality REQ-ER-SEQUESTERED-CWD criterion=length — title is 742 words; want 3..=25
2026-09-06T13:09:01.7696351Z   [must] requirement_quality REQ-ER-SESSION-BRIEFING criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7697077Z   [must] requirement_quality REQ-ER-SESSION-BRIEFING criterion=length — title is 579 words; want 3..=25
2026-09-06T13:09:01.7697783Z   [must] requirement_quality REQ-EVENT-ATTR-PASSTHROUGH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7698485Z   [must] requirement_quality REQ-EVENT-ATTR-PASSTHROUGH criterion=length — title is 281 words; want 3..=25
2026-09-06T13:09:01.7699281Z   [must] requirement_quality REQ-FIXTURE-BIN-BUILD-EDGE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7700054Z   [must] requirement_quality REQ-FIXTURE-BIN-BUILD-EDGE criterion=length — title is 910 words; want 3..=25
2026-09-06T13:09:01.7700789Z   [must] requirement_quality REQ-FIXTURE-BIN-PERJOB-DRIFT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7701499Z   [must] requirement_quality REQ-FIXTURE-BIN-PERJOB-DRIFT criterion=length — title is 845 words; want 3..=25
2026-09-06T13:09:01.7702211Z   [must] requirement_quality REQ-FLAKE-LEDGER-KEYED-ROWS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7702917Z   [must] requirement_quality REQ-FLAKE-LEDGER-KEYED-ROWS criterion=length — title is 344 words; want 3..=25
2026-09-06T13:09:01.7703616Z   [must] requirement_quality REQ-FORK-ACCESS-GATED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7704279Z   [must] requirement_quality REQ-FORK-ACCESS-GATED criterion=length — title is 396 words; want 3..=25
2026-09-06T13:09:01.7704965Z   [must] requirement_quality REQ-FORK-CONTROL-SURFACE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7705770Z   [must] requirement_quality REQ-FORK-CONTROL-SURFACE criterion=length — title is 812 words; want 3..=25
2026-09-06T13:09:01.7706450Z   [must] requirement_quality REQ-FORK-MIND-COMPLETE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7707117Z   [must] requirement_quality REQ-FORK-MIND-COMPLETE criterion=length — title is 343 words; want 3..=25
2026-09-06T13:09:01.7707842Z   [must] requirement_quality REQ-GOLDEN-CI-LANE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7708494Z   [must] requirement_quality REQ-GOLDEN-CI-LANE criterion=length — title is 133 words; want 3..=25
2026-09-06T13:09:01.7709298Z   [must] requirement_quality REQ-GOLDEN-RESPIN-TEST criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7709966Z   [must] requirement_quality REQ-GOLDEN-RESPIN-TEST criterion=length — title is 246 words; want 3..=25
2026-09-06T13:09:01.7710680Z   [must] requirement_quality REQ-GOSSIP-ADAPTER-PROJECTS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7711387Z   [must] requirement_quality REQ-GOSSIP-ADAPTER-PROJECTS criterion=length — title is 82 words; want 3..=25
2026-09-06T13:09:01.7712102Z   [must] requirement_quality REQ-GOSSIP-CONTROLLED-ANY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7712866Z   [must] requirement_quality REQ-GOSSIP-CONTROLLED-ANY criterion=length — title is 167 words; want 3..=25
2026-09-06T13:09:01.7713491Z   [must] requirement_quality REQ-GOSSIP-CONTROLLED-CROSS-NODE criterion=length — title is 108 words; want 3..=25
2026-09-06T13:09:01.7714138Z   [must] requirement_quality REQ-GOSSIP-PROJECT-DERIVE-ONCE criterion=length — title is 160 words; want 3..=25
2026-09-06T13:09:01.7714910Z   [must] requirement_quality REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7715689Z   [must] requirement_quality REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP criterion=length — title is 199 words; want 3..=25
2026-09-06T13:09:01.7716382Z   [must] requirement_quality REQ-HAZARD-ADAPTER-PROFILE-STAMP-CLOBBER criterion=length — title is 114 words; want 3..=25
2026-09-06T13:09:01.7717062Z   [must] requirement_quality REQ-HAZARD-ATOMIC-TMP-COLLISION criterion=length — title is 29 words; want 3..=25
2026-09-06T13:09:01.7717786Z   [must] requirement_quality REQ-HAZARD-ATTACH-WEDGE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7718464Z   [must] requirement_quality REQ-HAZARD-ATTACH-WEDGE criterion=length — title is 244 words; want 3..=25
2026-09-06T13:09:01.7719272Z   [must] requirement_quality REQ-HAZARD-BIND-CONFLICT-PID-ABA criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7720024Z   [must] requirement_quality REQ-HAZARD-BIND-CONFLICT-PID-ABA criterion=length — title is 741 words; want 3..=25
2026-09-06T13:09:01.7720740Z   [must] requirement_quality REQ-HAZARD-BIND-CWD-UNSET criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7721428Z   [must] requirement_quality REQ-HAZARD-BIND-CWD-UNSET criterion=length — title is 130 words; want 3..=25
2026-09-06T13:09:01.7722158Z   [must] requirement_quality REQ-HAZARD-BIND-REST-STATE-CARRY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7722887Z   [must] requirement_quality REQ-HAZARD-BIND-REST-STATE-CARRY criterion=length — title is 115 words; want 3..=25
2026-09-06T13:09:01.7723641Z   [must] requirement_quality REQ-HAZARD-BOUNDARY-READY-STRAND criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7724371Z   [must] requirement_quality REQ-HAZARD-BOUNDARY-READY-STRAND criterion=length — title is 175 words; want 3..=25
2026-09-06T13:09:01.7725222Z   [must] requirement_quality REQ-HAZARD-BRAIN-RESPAWN-PATH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7725944Z   [must] requirement_quality REQ-HAZARD-BRAIN-RESPAWN-PATH criterion=length — title is 119 words; want 3..=25
2026-09-06T13:09:01.7726738Z   [must] requirement_quality REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7727628Z   [must] requirement_quality REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE criterion=length — title is 125 words; want 3..=25
2026-09-06T13:09:01.7728443Z   [must] requirement_quality REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7729296Z   [must] requirement_quality REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP criterion=length — title is 199 words; want 3..=25
2026-09-06T13:09:01.7730089Z   [must] requirement_quality REQ-HAZARD-BROKER-FLOOR-LOCK-POISON criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7730847Z   [must] requirement_quality REQ-HAZARD-BROKER-FLOOR-LOCK-POISON criterion=length — title is 333 words; want 3..=25
2026-09-06T13:09:01.7731615Z   [must] requirement_quality REQ-HAZARD-BROKER-PROCESS-ISOLATION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7732432Z   [must] requirement_quality REQ-HAZARD-BROKER-PROCESS-ISOLATION criterion=length — title is 114 words; want 3..=25
2026-09-06T13:09:01.7733194Z   [must] requirement_quality REQ-HAZARD-BROKER-QUIC-DEADLINE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7733925Z   [must] requirement_quality REQ-HAZARD-BROKER-QUIC-DEADLINE criterion=length — title is 162 words; want 3..=25
2026-09-06T13:09:01.7734672Z   [must] requirement_quality REQ-HAZARD-BROKER-SEED-WIRE-SKEW criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7735410Z   [must] requirement_quality REQ-HAZARD-BROKER-SEED-WIRE-SKEW criterion=length — title is 193 words; want 3..=25
2026-09-06T13:09:01.7736189Z   [must] requirement_quality REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7736998Z   [must] requirement_quality REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE criterion=length — title is 192 words; want 3..=25
2026-09-06T13:09:01.7737681Z   [must] requirement_quality REQ-HAZARD-CEREMONY-CLOCK-STEP criterion=length — title is 139 words; want 3..=25
2026-09-06T13:09:01.7738422Z   [must] requirement_quality REQ-HAZARD-CODE-SEAL-SEED-ORACLE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7739241Z   [must] requirement_quality REQ-HAZARD-CODE-SEAL-SEED-ORACLE criterion=length — title is 236 words; want 3..=25
2026-09-06T13:09:01.7739913Z   [must] requirement_quality REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE criterion=length — title is 263 words; want 3..=25
2026-09-06T13:09:01.7740592Z   [must] requirement_quality REQ-HAZARD-CONFLICT-BOTH-PRESERVED criterion=length — title is 29 words; want 3..=25
2026-09-06T13:09:01.7741350Z   [must] requirement_quality REQ-HAZARD-CONTROL-STAMP-CONVERGENCE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7742117Z   [must] requirement_quality REQ-HAZARD-CONTROL-STAMP-CONVERGENCE criterion=length — title is 193 words; want 3..=25
2026-09-06T13:09:01.7742865Z   [must] requirement_quality REQ-HAZARD-CONTROL-STAMP-LIFETIME criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7743588Z   [must] requirement_quality REQ-HAZARD-CONTROL-STAMP-LIFETIME criterion=length — title is 100 words; want 3..=25
2026-09-06T13:09:01.7744327Z   [must] requirement_quality REQ-HAZARD-CONTROLLER-GAP-RESUME criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7745176Z   [must] requirement_quality REQ-HAZARD-CONTROLLER-GAP-RESUME criterion=length — title is 297 words; want 3..=25
2026-09-06T13:09:01.7745970Z   [must] requirement_quality REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7746863Z   [must] requirement_quality REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND criterion=length — title is 134 words; want 3..=25
2026-09-06T13:09:01.7747650Z   [must] requirement_quality REQ-HAZARD-CONTROLLER-LEASE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7748366Z   [must] requirement_quality REQ-HAZARD-CONTROLLER-LEASE criterion=length — title is 124 words; want 3..=25
2026-09-06T13:09:01.7749205Z   [must] requirement_quality REQ-HAZARD-CONTROLLER-RETAKE-FLOOR criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7749969Z   [must] requirement_quality REQ-HAZARD-CONTROLLER-RETAKE-FLOOR criterion=length — title is 184 words; want 3..=25
2026-09-06T13:09:01.7750744Z   [must] requirement_quality REQ-HAZARD-CONTROLLER-WRITER-REORDER criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7751510Z   [must] requirement_quality REQ-HAZARD-CONTROLLER-WRITER-REORDER criterion=length — title is 643 words; want 3..=25
2026-09-06T13:09:01.7752372Z   [must] requirement_quality REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7753156Z   [must] requirement_quality REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE criterion=length — title is 275 words; want 3..=25
2026-09-06T13:09:01.7753844Z   [must] requirement_quality REQ-HAZARD-DAEMON-SCHED-NONBLOCKING criterion=length — title is 32 words; want 3..=25
2026-09-06T13:09:01.7754596Z   [must] requirement_quality REQ-HAZARD-DAEMON-STOP-BARRIER criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7755336Z   [must] requirement_quality REQ-HAZARD-DAEMON-STOP-BARRIER criterion=length — title is 80 words; want 3..=25
2026-09-06T13:09:01.7756065Z   [must] requirement_quality REQ-HAZARD-DAEMON-STOP-REAP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7756784Z   [must] requirement_quality REQ-HAZARD-DAEMON-STOP-REAP criterion=length — title is 90 words; want 3..=25
2026-09-06T13:09:01.7757489Z   [must] requirement_quality REQ-HAZARD-DEAD-REC-PID criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7758167Z   [must] requirement_quality REQ-HAZARD-DEAD-REC-PID criterion=length — title is 175 words; want 3..=25
2026-09-06T13:09:01.7758874Z   [must] requirement_quality REQ-HAZARD-DEFERRED-MANIFEST criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7759684Z   [must] requirement_quality REQ-HAZARD-DEFERRED-MANIFEST criterion=length — title is 112 words; want 3..=25
2026-09-06T13:09:01.7760418Z   [must] requirement_quality REQ-HAZARD-DELIVERY-STARVATION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7761154Z   [must] requirement_quality REQ-HAZARD-DELIVERY-STARVATION criterion=length — title is 151 words; want 3..=25
2026-09-06T13:09:01.7761889Z   [must] requirement_quality REQ-HAZARD-DETACHED-DAEMON-STDIO criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7762627Z   [must] requirement_quality REQ-HAZARD-DETACHED-DAEMON-STDIO criterion=length — title is 255 words; want 3..=25
2026-09-06T13:09:01.7763285Z   [must] requirement_quality REQ-HAZARD-DETACHED-PIPE-INHERIT criterion=length — title is 52 words; want 3..=25
2026-09-06T13:09:01.7764047Z   [must] requirement_quality REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7764927Z   [must] requirement_quality REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT criterion=length — title is 232 words; want 3..=25
2026-09-06T13:09:01.7765681Z   [must] requirement_quality REQ-HAZARD-DRIVEN-BY-SELFHEAL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7766434Z   [must] requirement_quality REQ-HAZARD-DRIVEN-BY-SELFHEAL criterion=length — title is 77 words; want 3..=25
2026-09-06T13:09:01.7767206Z   [must] requirement_quality REQ-HAZARD-ECHO-NEVER-WRITES-AGENT-DROP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7767983Z   [must] requirement_quality REQ-HAZARD-ECHO-NEVER-WRITES-AGENT-DROP criterion=length — title is 159 words; want 3..=25
2026-09-06T13:09:01.7768751Z   [must] requirement_quality REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7769601Z   [must] requirement_quality REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE criterion=length — title is 440 words; want 3..=25
2026-09-06T13:09:01.7770349Z   [must] requirement_quality REQ-HAZARD-ELEVATED-DAEMON-SPAWN criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7771070Z   [must] requirement_quality REQ-HAZARD-ELEVATED-DAEMON-SPAWN criterion=length — title is 58 words; want 3..=25
2026-09-06T13:09:01.7771883Z   [must] requirement_quality REQ-HAZARD-EMPOWER-SESSION-RESURRECT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7772635Z   [must] requirement_quality REQ-HAZARD-EMPOWER-SESSION-RESURRECT criterion=length — title is 625 words; want 3..=25
2026-09-06T13:09:01.7773374Z   [must] requirement_quality REQ-HAZARD-ENDPOINT-LIFECYCLE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7774074Z   [must] requirement_quality REQ-HAZARD-ENDPOINT-LIFECYCLE criterion=length — title is 99 words; want 3..=25
2026-09-06T13:09:01.7774819Z   [must] requirement_quality REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7775588Z   [must] requirement_quality REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT criterion=length — title is 228 words; want 3..=25
2026-09-06T13:09:01.7776308Z   [must] requirement_quality REQ-HAZARD-ENV-SUBST criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7776956Z   [must] requirement_quality REQ-HAZARD-ENV-SUBST criterion=length — title is 168 words; want 3..=25
2026-09-06T13:09:01.7777656Z   [must] requirement_quality REQ-HAZARD-ENVELOPE-ATTR-LINESAFE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7778391Z   [must] requirement_quality REQ-HAZARD-ENVELOPE-ATTR-LINESAFE criterion=length — title is 179 words; want 3..=25
2026-09-06T13:09:01.7779205Z   [must] requirement_quality REQ-HAZARD-ENVELOPE-CR-LINESAFE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7779926Z   [must] requirement_quality REQ-HAZARD-ENVELOPE-CR-LINESAFE criterion=length — title is 73 words; want 3..=25
2026-09-06T13:09:01.7780671Z   [must] requirement_quality REQ-HAZARD-ENVELOPE-PARSER-SAFE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7781410Z   [must] requirement_quality REQ-HAZARD-EPOCH-RESET criterion=length — title is 60 words; want 3..=25
2026-09-06T13:09:01.7782130Z   [must] requirement_quality REQ-HAZARD-FAILED-ADDR-REINSTALL-LOOP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7782885Z   [must] requirement_quality REQ-HAZARD-FAILED-ADDR-REINSTALL-LOOP criterion=length — title is 176 words; want 3..=25
2026-09-06T13:09:01.7783605Z   [must] requirement_quality REQ-HAZARD-GEN-START-NOW criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7784430Z   [must] requirement_quality REQ-HAZARD-HOSTED-LIVENESS-RECONCILE criterion=length — title is 175 words; want 3..=25
2026-09-06T13:09:01.7785200Z   [must] requirement_quality REQ-HAZARD-IDLE-SILENT-NONDELIVERY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7785971Z   [must] requirement_quality REQ-HAZARD-IDLE-SILENT-NONDELIVERY criterion=length — title is 436 words; want 3..=25
2026-09-06T13:09:01.7786726Z   [must] requirement_quality REQ-HAZARD-INJECT-CONTROL-COEXIST criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7787473Z   [must] requirement_quality REQ-HAZARD-INJECT-CONTROL-COEXIST criterion=length — title is 340 words; want 3..=25
2026-09-06T13:09:01.7788211Z   [must] requirement_quality REQ-HAZARD-INJECT-SETTLE-REARM criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7789027Z   [must] requirement_quality REQ-HAZARD-INJECT-SETTLE-REARM criterion=length — title is 184 words; want 3..=25
2026-09-06T13:09:01.7789765Z   [must] requirement_quality REQ-HAZARD-INJECT-WORKER-POISON criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7790501Z   [must] requirement_quality REQ-HAZARD-INJECT-WORKER-POISON criterion=length — title is 136 words; want 3..=25
2026-09-06T13:09:01.7791308Z   [must] requirement_quality REQ-HAZARD-INPUT-ACK-BACKPRESSURE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7792060Z   [must] requirement_quality REQ-HAZARD-INPUT-ACK-BACKPRESSURE criterion=length — title is 343 words; want 3..=25
2026-09-06T13:09:01.7792704Z   [must] requirement_quality REQ-HAZARD-INSTANT-UNDERFLOW criterion=length — title is 30 words; want 3..=25
2026-09-06T13:09:01.7793431Z   [must] requirement_quality REQ-HAZARD-LISTEN-ORPHAN criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7794132Z   [must] requirement_quality REQ-HAZARD-LISTEN-ORPHAN criterion=length — title is 93 words; want 3..=25
2026-09-06T13:09:01.7794781Z   [must] requirement_quality REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE criterion=length — title is 122 words; want 3..=25
2026-09-06T13:09:01.7799668Z   [must] requirement_quality REQ-HAZARD-LIVEHOST-BOOT-RACE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7800597Z   [must] requirement_quality REQ-HAZARD-LIVEHOST-BOOT-RACE criterion=length — title is 158 words; want 3..=25
2026-09-06T13:09:01.7801357Z   [must] requirement_quality REQ-HAZARD-LIVEHOST-NONRESIDENT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7802078Z   [must] requirement_quality REQ-HAZARD-LIVEHOST-NONRESIDENT criterion=length — title is 171 words; want 3..=25
2026-09-06T13:09:01.7802797Z   [must] requirement_quality REQ-HAZARD-MESH-BOOTSTRAP-TRAP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7803516Z   [must] requirement_quality REQ-HAZARD-MESH-BOOTSTRAP-TRAP criterion=length — title is 154 words; want 3..=25
2026-09-06T13:09:01.7804267Z   [must] requirement_quality REQ-HAZARD-MONITOR-GREEN-SELF-PRODUCIBLE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7805149Z   [must] requirement_quality REQ-HAZARD-MONITOR-GREEN-SELF-PRODUCIBLE criterion=length — title is 402 words; want 3..=25
2026-09-06T13:09:01.7805585Z   [must] requirement_quality REQ-HAZARD-MUTUAL-PREAUTH-SIBLING criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7805909Z   [must] requirement_quality REQ-HAZARD-MUTUAL-PREAUTH-SIBLING criterion=length — title is 317 words; want 3..=25
2026-09-06T13:09:01.7806226Z   [must] requirement_quality REQ-HAZARD-PAIR-RATE-LIMIT criterion=length — title is 37 words; want 3..=25
2026-09-06T13:09:01.7806663Z   [must] requirement_quality REQ-HAZARD-PAIR-SEED-ROTATION criterion=length — title is 33 words; want 3..=25
2026-09-06T13:09:01.7807081Z   [must] requirement_quality REQ-HAZARD-PAIR-TRANSCRIPT-BIND criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7807535Z   [must] requirement_quality REQ-HAZARD-PERCH-GC-LIVENESS-INVERSION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7807907Z   [must] requirement_quality REQ-HAZARD-PERCH-GC-LIVENESS-INVERSION criterion=length — title is 184 words; want 3..=25
2026-09-06T13:09:01.7808374Z   [must] requirement_quality REQ-HAZARD-PROBE-CONTEXT-ISOLATION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7808718Z   [must] requirement_quality REQ-HAZARD-PROBE-CONTEXT-ISOLATION criterion=length — title is 170 words; want 3..=25
2026-09-06T13:09:01.7809288Z   [must] requirement_quality REQ-HAZARD-PROCESS-GLOBAL-ONE-RESOLUTION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7809655Z   [must] requirement_quality REQ-HAZARD-PROCESS-GLOBAL-ONE-RESOLUTION criterion=length — title is 127 words; want 3..=25
2026-09-06T13:09:01.7810081Z   [must] requirement_quality REQ-HAZARD-PSYCHE-OUTBOUND-PROXY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7810495Z   [must] requirement_quality REQ-HAZARD-PSYCHE-OUTBOUND-PROXY criterion=length — title is 27 words; want 3..=25
2026-09-06T13:09:01.7810948Z   [must] requirement_quality REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7811304Z   [must] requirement_quality REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION criterion=length — title is 130 words; want 3..=25
2026-09-06T13:09:01.7811732Z   [must] requirement_quality REQ-HAZARD-PTY-INPUT-WRITER-WEDGE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7812070Z   [must] requirement_quality REQ-HAZARD-PTY-INPUT-WRITER-WEDGE criterion=length — title is 287 words; want 3..=25
2026-09-06T13:09:01.7812484Z   [must] requirement_quality REQ-HAZARD-PUMP-IPC-DEADLINE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7812809Z   [must] requirement_quality REQ-HAZARD-PUMP-IPC-DEADLINE criterion=length — title is 38 words; want 3..=25
2026-09-06T13:09:01.7813224Z   [must] requirement_quality REQ-HAZARD-RC-ATTACH-FAILFAST criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7813549Z   [must] requirement_quality REQ-HAZARD-RC-ATTACH-FAILFAST criterion=length — title is 163 words; want 3..=25
2026-09-06T13:09:01.7813974Z   [must] requirement_quality REQ-HAZARD-RC-ATTACH-ONLINE-RACE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7814309Z   [must] requirement_quality REQ-HAZARD-RC-ATTACH-ONLINE-RACE criterion=length — title is 184 words; want 3..=25
2026-09-06T13:09:01.7814718Z   [must] requirement_quality REQ-HAZARD-RC-ATTACH-TRUTH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7815018Z   [must] requirement_quality REQ-HAZARD-RC-ATTACH-TRUTH criterion=length — title is 89 words; want 3..=25
2026-09-06T13:09:01.7815452Z   [must] requirement_quality REQ-HAZARD-RC-EOF criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7815729Z   [must] requirement_quality REQ-HAZARD-RC-EOF criterion=length — title is 208 words; want 3..=25
2026-09-06T13:09:01.7816149Z   [must] requirement_quality REQ-HAZARD-RC-INPUT-KEY-ENCODING criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7816479Z   [must] requirement_quality REQ-HAZARD-RC-INPUT-KEY-ENCODING criterion=length — title is 222 words; want 3..=25
2026-09-06T13:09:01.7817017Z   [must] requirement_quality REQ-HAZARD-REDISPATCH-CONTROL-STEAL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7817360Z   [must] requirement_quality REQ-HAZARD-REDISPATCH-CONTROL-STEAL criterion=length — title is 141 words; want 3..=25
2026-09-06T13:09:01.7817776Z   [must] requirement_quality REQ-HAZARD-REDISPATCH-STALL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7818124Z   [must] requirement_quality REQ-HAZARD-REDISPATCH-STALL criterion=length — title is 152 words; want 3..=25
2026-09-06T13:09:01.7818548Z   [must] requirement_quality REQ-HAZARD-REGISTRY-GHOST-ROWS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7818862Z   [must] requirement_quality REQ-HAZARD-REGISTRY-GHOST-ROWS criterion=length — title is 152 words; want 3..=25
2026-09-06T13:09:01.7819341Z   [must] requirement_quality REQ-HAZARD-REGISTRY-STALL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7819658Z   [must] requirement_quality REQ-HAZARD-REGISTRY-STALL criterion=length — title is 135 words; want 3..=25
2026-09-06T13:09:01.7819963Z   [must] requirement_quality REQ-HAZARD-RENDER-LIFECYCLE criterion=length — title is 85 words; want 3..=25
2026-09-06T13:09:01.7820446Z   [must] requirement_quality REQ-HAZARD-RESTART-STRANDS-PERSISTENT-SHELLS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7820826Z   [must] requirement_quality REQ-HAZARD-RESTART-STRANDS-PERSISTENT-SHELLS criterion=length — title is 387 words; want 3..=25
2026-09-06T13:09:01.7821151Z   [must] requirement_quality REQ-HAZARD-RESUME-CUSTODY-ABA criterion=length — title is 68 words; want 3..=25
2026-09-06T13:09:01.7821561Z   [must] requirement_quality REQ-HAZARD-RING-PERCH-ADOPTION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7821884Z   [must] requirement_quality REQ-HAZARD-RING-PERCH-ADOPTION criterion=length — title is 566 words; want 3..=25
2026-09-06T13:09:01.7822206Z   [must] requirement_quality REQ-HAZARD-ROLLBACK-STATE-COMPAT criterion=length — title is 72 words; want 3..=25
2026-09-06T13:09:01.7822588Z   [must] requirement_quality REQ-HAZARD-ROSTER-GHOST criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7822884Z   [must] requirement_quality REQ-HAZARD-ROSTER-GHOST criterion=length — title is 116 words; want 3..=25
2026-09-06T13:09:01.7823222Z   [must] requirement_quality REQ-HAZARD-SEEDMAP-CONNECT-UNBOUNDED criterion=length — title is 171 words; want 3..=25
2026-09-06T13:09:01.7823612Z   [must] requirement_quality REQ-HAZARD-SELF-DETECT-TIE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7823913Z   [must] requirement_quality REQ-HAZARD-SELF-DETECT-TIE criterion=length — title is 284 words; want 3..=25
2026-09-06T13:09:01.7824303Z   [must] requirement_quality REQ-HAZARD-SELF-ELEVATE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7824594Z   [must] requirement_quality REQ-HAZARD-SELF-ELEVATE criterion=length — title is 101 words; want 3..=25
2026-09-06T13:09:01.7825015Z   [must] requirement_quality REQ-HAZARD-SELF-ROW-LEASE-FREEZE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7825392Z   [must] requirement_quality REQ-HAZARD-SELF-ROW-LEASE-FREEZE criterion=length — title is 183 words; want 3..=25
2026-09-06T13:09:01.7825794Z   [must] requirement_quality REQ-HAZARD-SESSION-PIN-WEDGE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7826099Z   [must] requirement_quality REQ-HAZARD-SESSION-PIN-WEDGE criterion=length — title is 320 words; want 3..=25
2026-09-06T13:09:01.7826598Z   [must] requirement_quality REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7827086Z   [must] requirement_quality REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK criterion=length — title is 461 words; want 3..=25
2026-09-06T13:09:01.7827487Z   [must] requirement_quality REQ-HAZARD-SHELL-STALE-ONLINE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7827859Z   [must] requirement_quality REQ-HAZARD-SHELL-STALE-ONLINE criterion=length — title is 1795 words; want 3..=25
2026-09-06T13:09:01.7828197Z   [must] requirement_quality REQ-HAZARD-SPOOL-SENTINEL-CREATE-FAIL criterion=length — title is 84 words; want 3..=25
2026-09-06T13:09:01.7828647Z   [must] requirement_quality REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7829080Z   [must] requirement_quality REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP criterion=length — title is 120 words; want 3..=25
2026-09-06T13:09:01.7829403Z   [must] requirement_quality REQ-HAZARD-STOP-RESPAWN-CONVOY criterion=length — title is 62 words; want 3..=25
2026-09-06T13:09:01.7829805Z   [must] requirement_quality REQ-HAZARD-STORE-INIT-RACE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7830113Z   [must] requirement_quality REQ-HAZARD-STORE-INIT-RACE criterion=length — title is 149 words; want 3..=25
2026-09-06T13:09:01.7830409Z   [must] requirement_quality REQ-HAZARD-SUDO-SECURE-PATH criterion=length — title is 43 words; want 3..=25
2026-09-06T13:09:01.7830814Z   [must] requirement_quality REQ-HAZARD-TEARDOWN-DEADEND criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7831114Z   [must] requirement_quality REQ-HAZARD-TEARDOWN-DEADEND criterion=length — title is 240 words; want 3..=25
2026-09-06T13:09:01.7831526Z   [must] requirement_quality REQ-HAZARD-TEMPLATE-ARGV-FILL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7831855Z   [must] requirement_quality REQ-HAZARD-TEMPLATE-ARGV-FILL criterion=length — title is 166 words; want 3..=25
2026-09-06T13:09:01.7832297Z   [must] requirement_quality REQ-HAZARD-TEST-PID-TREE-KILL-IDENTITY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7832647Z   [must] requirement_quality REQ-HAZARD-TEST-PID-TREE-KILL-IDENTITY criterion=length — title is 305 words; want 3..=25
2026-09-06T13:09:01.7832957Z   [must] requirement_quality REQ-HAZARD-THRASH-GUARD-BLIND criterion=length — title is 62 words; want 3..=25
2026-09-06T13:09:01.7833429Z   [must] requirement_quality REQ-HAZARD-TRANSLATE-FAULT-PERMANENT-DEATH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7833792Z   [must] requirement_quality REQ-HAZARD-TRANSLATE-FAULT-PERMANENT-DEATH criterion=length — title is 91 words; want 3..=25
2026-09-06T13:09:01.7834187Z   [must] requirement_quality REQ-HAZARD-TREE-CWD-PIN criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7834483Z   [must] requirement_quality REQ-HAZARD-TREE-CWD-PIN criterion=length — title is 581 words; want 3..=25
2026-09-06T13:09:01.7834902Z   [must] requirement_quality REQ-HAZARD-UNHOST-PSYCHE-REAP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7835306Z   [must] requirement_quality REQ-HAZARD-UNHOST-PSYCHE-REAP criterion=length — title is 161 words; want 3..=25
2026-09-06T13:09:01.7835734Z   [must] requirement_quality REQ-HAZARD-VIEWER-CLOSE-DETACH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7836053Z   [must] requirement_quality REQ-HAZARD-VIEWER-CLOSE-DETACH criterion=length — title is 437 words; want 3..=25
2026-09-06T13:09:01.7836459Z   [must] requirement_quality REQ-HAZARD-VIEWER-ISOLATION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7836863Z   [must] requirement_quality REQ-HAZARD-VIEWER-ISOLATION criterion=length — title is 118 words; want 3..=25
2026-09-06T13:09:01.7837298Z   [must] requirement_quality REQ-HAZARD-VIEWER-RING-ROLL-SNAP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7837628Z   [must] requirement_quality REQ-HAZARD-VIEWER-RING-ROLL-SNAP criterion=length — title is 167 words; want 3..=25
2026-09-06T13:09:01.7838200Z   [must] requirement_quality REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7838625Z   [must] requirement_quality REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE criterion=length — title is 235 words; want 3..=25
2026-09-06T13:09:01.7838930Z   [must] requirement_quality REQ-HAZARD-WAN-ORIGIN-AUTH criterion=length — title is 37 words; want 3..=25
2026-09-06T13:09:01.7839453Z   [must] requirement_quality REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7839793Z   [must] requirement_quality REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE criterion=length — title is 96 words; want 3..=25
2026-09-06T13:09:01.7840217Z   [must] requirement_quality REQ-HAZARD-WIRE-UNTAGGED-RECORD criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7840551Z   [must] requirement_quality REQ-HAZARD-WIRE-UNTAGGED-RECORD criterion=length — title is 332 words; want 3..=25
2026-09-06T13:09:01.7840860Z   [must] requirement_quality REQ-HAZARD-WMI-DAEMON-WINDOW criterion=length — title is 101 words; want 3..=25
2026-09-06T13:09:01.7841269Z   [must] requirement_quality REQ-HEAVY-UNIT-CLASSIFICATION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7841589Z   [must] requirement_quality REQ-HEAVY-UNIT-CLASSIFICATION criterion=length — title is 197 words; want 3..=25
2026-09-06T13:09:01.7841861Z   [must] requirement_quality REQ-HOST-RUN-1 criterion=length — title is 88 words; want 3..=25
2026-09-06T13:09:01.7842119Z   [must] requirement_quality REQ-HOST-RUN-2 criterion=length — title is 97 words; want 3..=25
2026-09-06T13:09:01.7842556Z   [must] requirement_quality REQ-HOSTING-AUTHORITY-CONTROLLABLE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7842899Z   [must] requirement_quality REQ-HOSTING-AUTHORITY-CONTROLLABLE criterion=length — title is 302 words; want 3..=25
2026-09-06T13:09:01.7843297Z   [must] requirement_quality REQ-IDLE-PARKED-DELIVERY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7843592Z   [must] requirement_quality REQ-IDLE-PARKED-DELIVERY criterion=length — title is 116 words; want 3..=25
2026-09-06T13:09:01.7844047Z   [must] requirement_quality REQ-INBOUND-LINUX-HOST-FIREWALL-VERIFY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7844404Z   [must] requirement_quality REQ-INBOUND-LINUX-HOST-FIREWALL-VERIFY criterion=length — title is 181 words; want 3..=25
2026-09-06T13:09:01.7844838Z   [must] requirement_quality REQ-INBOUND-PROVIDER-FIREWALL-DOCS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7845239Z   [must] requirement_quality REQ-INBOUND-PROVIDER-FIREWALL-DOCS criterion=length — title is 98 words; want 3..=25
2026-09-06T13:09:01.7845659Z   [must] requirement_quality REQ-INBOUND-RECORD-WIRE-FIDELITY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7845990Z   [must] requirement_quality REQ-INBOUND-RECORD-WIRE-FIDELITY criterion=length — title is 492 words; want 3..=25
2026-09-06T13:09:01.7846445Z   [must] requirement_quality REQ-INBOUND-VERDICT-RECORD-BINDER-PINNED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7846919Z   [must] requirement_quality REQ-INBOUND-VERDICT-RECORD-BINDER-PINNED criterion=length — title is 292 words; want 3..=25
2026-09-06T13:09:01.7847328Z   [must] requirement_quality REQ-INBOUND-VERDICT-SURFACED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7847647Z   [must] requirement_quality REQ-INBOUND-VERDICT-SURFACED criterion=length — title is 642 words; want 3..=25
2026-09-06T13:09:01.7848178Z   [must] requirement_quality REQ-INBOUND-WINDOWS-RULE-BINDER-PATH-MATCH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7848547Z   [must] requirement_quality REQ-INBOUND-WINDOWS-RULE-BINDER-PATH-MATCH criterion=length — title is 263 words; want 3..=25
2026-09-06T13:09:01.7848865Z   [must] requirement_quality REQ-INJECT-MULTILINE-INTEGRITY criterion=length — title is 118 words; want 3..=25
2026-09-06T13:09:01.7849252Z   [must] requirement_quality REQ-INPUT-CONTROLLER-FENCE criterion=length — title is 190 words; want 3..=25
2026-09-06T13:09:01.7849509Z   [must] requirement_quality REQ-INST-15 criterion=length — title is 32 words; want 3..=25
2026-09-06T13:09:01.7849767Z   [must] requirement_quality REQ-INSTALL-1 criterion=length — title is 62 words; want 3..=25
2026-09-06T13:09:01.7850139Z   [must] requirement_quality REQ-INSTALL-10 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7850401Z   [must] requirement_quality REQ-INSTALL-10 criterion=length — title is 58 words; want 3..=25
2026-09-06T13:09:01.7850658Z   [must] requirement_quality REQ-INSTALL-11 criterion=length — title is 78 words; want 3..=25
2026-09-06T13:09:01.7850915Z   [must] requirement_quality REQ-INSTALL-12 criterion=length — title is 116 words; want 3..=25
2026-09-06T13:09:01.7851271Z   [must] requirement_quality REQ-INSTALL-13 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7851548Z   [must] requirement_quality REQ-INSTALL-13 criterion=length — title is 131 words; want 3..=25
2026-09-06T13:09:01.7851797Z   [must] requirement_quality REQ-INSTALL-2 criterion=length — title is 2 word(s); want 3..=25
2026-09-06T13:09:01.7852054Z   [must] requirement_quality REQ-INSTALL-5 criterion=length — title is 62 words; want 3..=25
2026-09-06T13:09:01.7852407Z   [must] requirement_quality REQ-INSTALL-6 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7852670Z   [must] requirement_quality REQ-INSTALL-6 criterion=length — title is 56 words; want 3..=25
2026-09-06T13:09:01.7853022Z   [must] requirement_quality REQ-INSTALL-7 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7853276Z   [must] requirement_quality REQ-INSTALL-7 criterion=length — title is 410 words; want 3..=25
2026-09-06T13:09:01.7853526Z   [must] requirement_quality REQ-INSTALL-8 criterion=length — title is 55 words; want 3..=25
2026-09-06T13:09:01.7853886Z   [must] requirement_quality REQ-INSTALL-9 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7854134Z   [must] requirement_quality REQ-INSTALL-9 criterion=length — title is 62 words; want 3..=25
2026-09-06T13:09:01.7854550Z   [must] requirement_quality REQ-INSTALL-BOOTSTRAP-VERB criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7854921Z   [must] requirement_quality REQ-INSTALL-BOOTSTRAP-VERB criterion=length — title is 177 words; want 3..=25
2026-09-06T13:09:01.7855312Z   [must] requirement_quality REQ-IO-BOUNDARY-EVENTS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7855607Z   [must] requirement_quality REQ-IO-BOUNDARY-EVENTS criterion=length — title is 553 words; want 3..=25
2026-09-06T13:09:01.7855989Z   [must] requirement_quality REQ-IO-COMMUNE-EVENT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7856374Z   [must] requirement_quality REQ-IO-COMMUNE-EVENT criterion=length — title is 244 words; want 3..=25
2026-09-06T13:09:01.7856777Z   [must] requirement_quality REQ-IO-COMMUNE-FAIL-EVENT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7857077Z   [must] requirement_quality REQ-IO-COMMUNE-FAIL-EVENT criterion=length — title is 163 words; want 3..=25
2026-09-06T13:09:01.7857569Z   [must] requirement_quality REQ-IO-DISPATCH-RESULT-VOCABULARY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7857903Z   [must] requirement_quality REQ-IO-DISPATCH-RESULT-VOCABULARY criterion=length — title is 160 words; want 3..=25
2026-09-06T13:09:01.7858298Z   [must] requirement_quality REQ-IO-DISPATCH-RESULTS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7858589Z   [must] requirement_quality REQ-IO-DISPATCH-RESULTS criterion=length — title is 145 words; want 3..=25
2026-09-06T13:09:01.7859074Z   [must] requirement_quality REQ-IO-EVENT-ADAPTER-LOG criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7859377Z   [must] requirement_quality REQ-IO-EVENT-ADAPTER-LOG criterion=length — title is 311 words; want 3..=25
2026-09-06T13:09:01.7859750Z   [must] requirement_quality REQ-IO-EVENT-BUS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7860027Z   [must] requirement_quality REQ-IO-EVENT-BUS criterion=length — title is 169 words; want 3..=25
2026-09-06T13:09:01.7860412Z   [must] requirement_quality REQ-IO-EVENT-POLL-VERB criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7860708Z   [must] requirement_quality REQ-IO-EVENT-POLL-VERB criterion=length — title is 517 words; want 3..=25
2026-09-06T13:09:01.7861093Z   [must] requirement_quality REQ-IO-EVENT-TAXONOMY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7861385Z   [must] requirement_quality REQ-IO-EVENT-TAXONOMY criterion=length — title is 183 words; want 3..=25
2026-09-06T13:09:01.7861784Z   [must] requirement_quality REQ-IO-INGEST-STATE-PAYLOAD criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7862089Z   [must] requirement_quality REQ-IO-INGEST-STATE-PAYLOAD criterion=length — title is 329 words; want 3..=25
2026-09-06T13:09:01.7862461Z   [must] requirement_quality REQ-IO-MIDTURN-SPAN criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7862732Z   [must] requirement_quality REQ-IO-MIDTURN-SPAN criterion=length — title is 372 words; want 3..=25
2026-09-06T13:09:01.7863090Z   [must] requirement_quality REQ-IO-MSG-EDGES criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7863358Z   [must] requirement_quality REQ-IO-MSG-EDGES criterion=length — title is 148 words; want 3..=25
2026-09-06T13:09:01.7863772Z   [must] requirement_quality REQ-IO-SEAL-SHORTFORM-CEREMONY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7864086Z   [must] requirement_quality REQ-IO-SEAL-SHORTFORM-CEREMONY criterion=length — title is 193 words; want 3..=25
2026-09-06T13:09:01.7864552Z   [must] requirement_quality REQ-IO-SEAL-SHORTFORM-GRAMMAR criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7864867Z   [must] requirement_quality REQ-IO-SEAL-SHORTFORM-GRAMMAR criterion=length — title is 368 words; want 3..=25
2026-09-06T13:09:01.7865243Z   [must] requirement_quality REQ-IO-SEND-VERDICT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7865515Z   [must] requirement_quality REQ-IO-SEND-VERDICT criterion=length — title is 267 words; want 3..=25
2026-09-06T13:09:01.7865910Z   [must] requirement_quality REQ-IO-SHORTFORM-DISPATCH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7866311Z   [must] requirement_quality REQ-IO-SHORTFORM-DISPATCH criterion=length — title is 158 words; want 3..=25
2026-09-06T13:09:01.7866688Z   [must] requirement_quality REQ-IO-SHORTFORM-GATE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7867020Z   [must] requirement_quality REQ-IO-SHORTFORM-GATE criterion=length — title is 168 words; want 3..=25
2026-09-06T13:09:01.7867414Z   [must] requirement_quality REQ-IO-SUPPRESSION-GRAMMAR criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7867719Z   [must] requirement_quality REQ-IO-SUPPRESSION-GRAMMAR criterion=length — title is 168 words; want 3..=25
2026-09-06T13:09:01.7868157Z   [must] requirement_quality REQ-IPC-DEADLINE-UNHONORABLE-REFUSED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7868501Z   [must] requirement_quality REQ-IPC-DEADLINE-UNHONORABLE-REFUSED criterion=length — title is 491 words; want 3..=25
2026-09-06T13:09:01.7868916Z   [must] requirement_quality REQ-JOIN-DEFERRED-ELEVATION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7869308Z   [must] requirement_quality REQ-JOIN-DEFERRED-ELEVATION criterion=length — title is 156 words; want 3..=25
2026-09-06T13:09:01.7869737Z   [must] requirement_quality REQ-JOIN-DIAGNOSTICS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7870020Z   [must] requirement_quality REQ-JOIN-DIAGNOSTICS criterion=length — title is 137 words; want 3..=25
2026-09-06T13:09:01.7870296Z   [must] requirement_quality REQ-JOIN-TWO-PHASE criterion=length — title is 161 words; want 3..=25
2026-09-06T13:09:01.7870686Z   [must] requirement_quality REQ-JOIN-VERBOSE-CLOCK criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7870982Z   [must] requirement_quality REQ-JOIN-VERBOSE-CLOCK criterion=length — title is 108 words; want 3..=25
2026-09-06T13:09:01.7871344Z   [must] requirement_quality REQ-KICK-1 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7871597Z   [must] requirement_quality REQ-KICK-1 criterion=length — title is 133 words; want 3..=25
2026-09-06T13:09:01.7872002Z   [must] requirement_quality REQ-KNOCK-ANSWER-RECEIPT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7872302Z   [must] requirement_quality REQ-KNOCK-ANSWER-RECEIPT criterion=length — title is 417 words; want 3..=25
2026-09-06T13:09:01.7872697Z   [must] requirement_quality REQ-KNOCK-AUTHORITY-SPLIT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7872992Z   [must] requirement_quality REQ-KNOCK-AUTHORITY-SPLIT criterion=length — title is 271 words; want 3..=25
2026-09-06T13:09:01.7873374Z   [must] requirement_quality REQ-KNOCK-BARE-VERB criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7873655Z   [must] requirement_quality REQ-KNOCK-BARE-VERB criterion=length — title is 204 words; want 3..=25
2026-09-06T13:09:01.7874105Z   [must] requirement_quality REQ-KNOCK-CODE-INERT-RULE-REPAIR criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7874518Z   [must] requirement_quality REQ-KNOCK-CODE-INERT-RULE-REPAIR criterion=length — title is 276 words; want 3..=25
2026-09-06T13:09:01.7874935Z   [must] requirement_quality REQ-KNOCK-CODE-MULTI-ENVELOPE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7875254Z   [must] requirement_quality REQ-KNOCK-CODE-MULTI-ENVELOPE criterion=length — title is 703 words; want 3..=25
2026-09-06T13:09:01.7875641Z   [must] requirement_quality REQ-KNOCK-CODE-ROUTE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7876033Z   [must] requirement_quality REQ-KNOCK-CODE-ROUTE criterion=length — title is 309 words; want 3..=25
2026-09-06T13:09:01.7876413Z   [must] requirement_quality REQ-KNOCK-CODE-SEALED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7876704Z   [must] requirement_quality REQ-KNOCK-CODE-SEALED criterion=length — title is 420 words; want 3..=25
2026-09-06T13:09:01.7877181Z   [must] requirement_quality REQ-KNOCK-COUNTER-KNOCK-TRAVELS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7877508Z   [must] requirement_quality REQ-KNOCK-COUNTER-KNOCK-TRAVELS criterion=length — title is 400 words; want 3..=25
2026-09-06T13:09:01.7877926Z   [must] requirement_quality REQ-KNOCK-DEFAULT-SURFACE-MSG criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7878237Z   [must] requirement_quality REQ-KNOCK-DEFAULT-SURFACE-MSG criterion=length — title is 231 words; want 3..=25
2026-09-06T13:09:01.7878708Z   [must] requirement_quality REQ-KNOCK-DIRECTIONALITY-EXPLICIT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7879099Z   [must] requirement_quality REQ-KNOCK-DIRECTIONALITY-EXPLICIT criterion=length — title is 534 words; want 3..=25
2026-09-06T13:09:01.7879553Z   [must] requirement_quality REQ-KNOCK-DIRECTIONALITY-SENDER criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7879924Z   [must] requirement_quality REQ-KNOCK-DIRECTIONALITY-SENDER criterion=length — title is 452 words; want 3..=25
2026-09-06T13:09:01.7880340Z   [must] requirement_quality REQ-KNOCK-EVIDENCE-ROUTE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7880674Z   [must] requirement_quality REQ-KNOCK-EVIDENCE-ROUTE criterion=length — title is 416 words; want 3..=25
2026-09-06T13:09:01.7881055Z   [must] requirement_quality REQ-KNOCK-FLOOD criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7881322Z   [must] requirement_quality REQ-KNOCK-FLOOD criterion=length — title is 311 words; want 3..=25
2026-09-06T13:09:01.7881781Z   [must] requirement_quality REQ-KNOCK-HUMAN-PRESENTER-NOT-DEAD-ENDED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7882152Z   [must] requirement_quality REQ-KNOCK-HUMAN-PRESENTER-NOT-DEAD-ENDED criterion=length — title is 371 words; want 3..=25
2026-09-06T13:09:01.7882562Z   [must] requirement_quality REQ-KNOCK-INBOX-DAEMON-OWNED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7882881Z   [must] requirement_quality REQ-KNOCK-INBOX-DAEMON-OWNED criterion=length — title is 218 words; want 3..=25
2026-09-06T13:09:01.7883266Z   [must] requirement_quality REQ-KNOCK-KNOCKER-STAMP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7883692Z   [must] requirement_quality REQ-KNOCK-KNOCKER-STAMP criterion=length — title is 184 words; want 3..=25
2026-09-06T13:09:01.7884130Z   [must] requirement_quality REQ-KNOCK-MINT-MUTUAL-IS-A-REQUEST criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7884520Z   [must] requirement_quality REQ-KNOCK-MINT-MUTUAL-IS-A-REQUEST criterion=length — title is 689 words; want 3..=25
2026-09-06T13:09:01.7884922Z   [must] requirement_quality REQ-KNOCK-MONIC-IMPART criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7885235Z   [must] requirement_quality REQ-KNOCK-MONIC-IMPART criterion=length — title is 459 words; want 3..=25
2026-09-06T13:09:01.7885619Z   [must] requirement_quality REQ-KNOCK-MUTUAL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7885905Z   [must] requirement_quality REQ-KNOCK-MUTUAL criterion=length — title is 417 words; want 3..=25
2026-09-06T13:09:01.7886320Z   [must] requirement_quality REQ-KNOCK-NOTIFY-INSTIGATOR criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7886640Z   [must] requirement_quality REQ-KNOCK-NOTIFY-INSTIGATOR criterion=length — title is 706 words; want 3..=25
2026-09-06T13:09:01.7887083Z   [must] requirement_quality REQ-KNOCK-PRESCRIBED-REMEDY-HONORED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7887489Z   [must] requirement_quality REQ-KNOCK-PRESCRIBED-REMEDY-HONORED criterion=length — title is 458 words; want 3..=25
2026-09-06T13:09:01.7887913Z   [must] requirement_quality REQ-KNOCK-PRESCRIBES-FORK-PAIR criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7888235Z   [must] requirement_quality REQ-KNOCK-PRESCRIBES-FORK-PAIR criterion=length — title is 335 words; want 3..=25
2026-09-06T13:09:01.7888677Z   [must] requirement_quality REQ-KNOCK-REDEEM-MUTUAL-RECEIPT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7889078Z   [must] requirement_quality REQ-KNOCK-REDEEM-MUTUAL-RECEIPT criterion=length — title is 418 words; want 3..=25
2026-09-06T13:09:01.7889507Z   [must] requirement_quality REQ-KNOCK-REDEEM-SERVE-INTENT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7889840Z   [must] requirement_quality REQ-KNOCK-REDEEM-SERVE-INTENT criterion=length — title is 273 words; want 3..=25
2026-09-06T13:09:01.7890231Z   [must] requirement_quality REQ-KNOCK-REDEEM-WIRE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7890522Z   [must] requirement_quality REQ-KNOCK-REDEEM-WIRE criterion=length — title is 602 words; want 3..=25
2026-09-06T13:09:01.7890929Z   [must] requirement_quality REQ-KNOCK-UNRESOLVED-REFUSES criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7891257Z   [must] requirement_quality REQ-KNOCK-UNRESOLVED-REFUSES criterion=length — title is 449 words; want 3..=25
2026-09-06T13:09:01.7891620Z   [must] requirement_quality REQ-KNOCK-VERBS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7891897Z   [must] requirement_quality REQ-KNOCK-VERBS criterion=length — title is 201 words; want 3..=25
2026-09-06T13:09:01.7892313Z   [must] requirement_quality REQ-LIST-JSON-LIVENESS-PARITY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7892628Z   [must] requirement_quality REQ-LIST-JSON-LIVENESS-PARITY criterion=length — title is 240 words; want 3..=25
2026-09-06T13:09:01.7893084Z   [must] requirement_quality REQ-LISTEN-PRESERVES-HOSTING-TOPOLOGY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7893436Z   [must] requirement_quality REQ-LISTEN-PRESERVES-HOSTING-TOPOLOGY criterion=length — title is 404 words; want 3..=25
2026-09-06T13:09:01.7893971Z   [must] requirement_quality REQ-LISTEN-SEED-CONSUME-AFTER-BIND criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7894314Z   [must] requirement_quality REQ-LISTEN-SEED-CONSUME-AFTER-BIND criterion=length — title is 167 words; want 3..=25
2026-09-06T13:09:01.7894780Z   [must] requirement_quality REQ-LISTEN-SESSION-ID-FALLBACK criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7895100Z   [must] requirement_quality REQ-LISTEN-SESSION-ID-FALLBACK criterion=length — title is 185 words; want 3..=25
2026-09-06T13:09:01.7895530Z   [must] requirement_quality REQ-LIVE-AGENT-NO-INJECT-DELIVERY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7895867Z   [must] requirement_quality REQ-LIVE-AGENT-NO-INJECT-DELIVERY criterion=length — title is 312 words; want 3..=25
2026-09-06T13:09:01.7896313Z   [must] requirement_quality REQ-LIVEHOST-RECONCILE-TRIAL-SILENT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7896656Z   [must] requirement_quality REQ-LIVEHOST-RECONCILE-TRIAL-SILENT criterion=length — title is 214 words; want 3..=25
2026-09-06T13:09:01.7897153Z   [must] requirement_quality REQ-LIVENESS-ORACLE-SOUND criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7897519Z   [must] requirement_quality REQ-LIVENESS-ORACLE-SOUND criterion=length — title is 674 words; want 3..=25
2026-09-06T13:09:01.7897948Z   [must] requirement_quality REQ-LOCK-TOUCHING-LANE-PROCEDURE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7898277Z   [must] requirement_quality REQ-LOCK-TOUCHING-LANE-PROCEDURE criterion=length — title is 326 words; want 3..=25
2026-09-06T13:09:01.7898643Z   [must] requirement_quality REQ-MANIFEST-1 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7898912Z   [must] requirement_quality REQ-MANIFEST-3 criterion=length — title is 26 words; want 3..=25
2026-09-06T13:09:01.7899261Z   [must] requirement_quality REQ-MANIFEST-4 criterion=length — title is 31 words; want 3..=25
2026-09-06T13:09:01.7899528Z   [must] requirement_quality REQ-MANIFEST-5 criterion=length — title is 132 words; want 3..=25
2026-09-06T13:09:01.7899805Z   [must] requirement_quality REQ-MANIFEST-6 criterion=length — title is 84 words; want 3..=25
2026-09-06T13:09:01.7900171Z   [must] requirement_quality REQ-MANIFEST-7 criterion=length — title is 117 words; want 3..=25
2026-09-06T13:09:01.7900429Z   [must] requirement_quality REQ-MANIFEST-8 criterion=length — title is 77 words; want 3..=25
2026-09-06T13:09:01.7900815Z   [must] requirement_quality REQ-MANIFEST-NODE-KEY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7901129Z   [must] requirement_quality REQ-MANIFEST-NODE-KEY criterion=length — title is 187 words; want 3..=25
2026-09-06T13:09:01.7901522Z   [must] requirement_quality REQ-MANIFEST-SUBST criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7901799Z   [must] requirement_quality REQ-MANIFEST-SUBST criterion=length — title is 121 words; want 3..=25
2026-09-06T13:09:01.7902052Z   [must] requirement_quality REQ-MESH-1 criterion=length — title is 86 words; want 3..=25
2026-09-06T13:09:01.7902404Z   [must] requirement_quality REQ-MESH-2 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7902652Z   [must] requirement_quality REQ-MESH-2 criterion=length — title is 120 words; want 3..=25
2026-09-06T13:09:01.7903011Z   [must] requirement_quality REQ-MESH-3 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7903258Z   [must] requirement_quality REQ-MESH-3 criterion=length — title is 86 words; want 3..=25
2026-09-06T13:09:01.7903716Z   [must] requirement_quality REQ-MESH-4 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7904020Z   [must] requirement_quality REQ-MESH-4 criterion=length — title is 138 words; want 3..=25
2026-09-06T13:09:01.7904368Z   [must] requirement_quality REQ-MESH-5 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7904673Z   [must] requirement_quality REQ-MESH-5 criterion=length — title is 72 words; want 3..=25
2026-09-06T13:09:01.7905030Z   [must] requirement_quality REQ-MESH-6 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7905274Z   [must] requirement_quality REQ-MESH-6 criterion=length — title is 56 words; want 3..=25
2026-09-06T13:09:01.7905627Z   [must] requirement_quality REQ-MIGRATE-1 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7906042Z   [must] requirement_quality REQ-MONIC-DELIVERY-TRIGGER criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7906355Z   [must] requirement_quality REQ-MONIC-DELIVERY-TRIGGER criterion=length — title is 1117 words; want 3..=25
2026-09-06T13:09:01.7906749Z   [must] requirement_quality REQ-MONIC-HUSK-PRESENT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7907098Z   [must] requirement_quality REQ-MONIC-HUSK-PRESENT criterion=length — title is 508 words; want 3..=25
2026-09-06T13:09:01.7907464Z   [must] requirement_quality REQ-MONIC-STORE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7907732Z   [must] requirement_quality REQ-MONIC-STORE criterion=length — title is 846 words; want 3..=25
2026-09-06T13:09:01.7908094Z   [must] requirement_quality REQ-MONIC-VERBS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7908366Z   [must] requirement_quality REQ-MONIC-VERBS criterion=length — title is 758 words; want 3..=25
2026-09-06T13:09:01.7908610Z   [must] requirement_quality REQ-MSG-4 criterion=length — title is 31 words; want 3..=25
2026-09-06T13:09:01.7908851Z   [must] requirement_quality REQ-MSG-5 criterion=length — title is 38 words; want 3..=25
2026-09-06T13:09:01.7909184Z   [must] requirement_quality REQ-MSG-6 criterion=length — title is 65 words; want 3..=25
2026-09-06T13:09:01.7909564Z   [must] requirement_quality REQ-MSG-CLI-ORIGIN criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7909840Z   [must] requirement_quality REQ-MSG-CLI-ORIGIN criterion=length — title is 107 words; want 3..=25
2026-09-06T13:09:01.7910590Z   [must] requirement_quality REQ-MSG-DELIVERY-AXES criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7910876Z   [must] requirement_quality REQ-MSG-DELIVERY-AXES criterion=length — title is 291 words; want 3..=25
2026-09-06T13:09:01.7911290Z   [must] requirement_quality REQ-MSG-ENVELOPE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7911623Z   [must] requirement_quality REQ-MSG-ENVELOPE criterion=length — title is 153 words; want 3..=25
2026-09-06T13:09:01.7912059Z   [must] requirement_quality REQ-MSG-IDLE-EDGE-DRAIN criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7912378Z   [must] requirement_quality REQ-MSG-IDLE-EDGE-DRAIN criterion=length — title is 202 words; want 3..=25
2026-09-06T13:09:01.7912808Z   [must] requirement_quality REQ-MSG-IDLE-TRANSLATION-BINARY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7913137Z   [must] requirement_quality REQ-MSG-IDLE-TRANSLATION-BINARY criterion=length — title is 269 words; want 3..=25
2026-09-06T13:09:01.7913566Z   [must] requirement_quality REQ-MSG-INJECT-LEG-DROP-VISIBLE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7914014Z   [must] requirement_quality REQ-MSG-INJECT-LEG-DROP-VISIBLE criterion=length — title is 239 words; want 3..=25
2026-09-06T13:09:01.7914425Z   [must] requirement_quality REQ-MSG-SELF-DETECT-ANCESTRY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7914829Z   [must] requirement_quality REQ-MSG-SELF-DETECT-ANCESTRY criterion=length — title is 153 words; want 3..=25
2026-09-06T13:09:01.7915243Z   [must] requirement_quality REQ-MSG-SENDER-STAMP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7915533Z   [must] requirement_quality REQ-MSG-SENDER-STAMP criterion=length — title is 537 words; want 3..=25
2026-09-06T13:09:01.7915915Z   [must] requirement_quality REQ-NET-FAMILY-GATE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7916206Z   [must] requirement_quality REQ-NET-FAMILY-GATE criterion=length — title is 118 words; want 3..=25
2026-09-06T13:09:01.7916607Z   [must] requirement_quality REQ-NET-PINNED-UDP-PORT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7916906Z   [must] requirement_quality REQ-NET-PINNED-UDP-PORT criterion=length — title is 93 words; want 3..=25
2026-09-06T13:09:01.7917237Z   [must] requirement_quality REQ-NOTIF-COALESCE criterion=length — title is 44 words; want 3..=25
2026-09-06T13:09:01.7917653Z   [must] requirement_quality REQ-NOTIF-DRAIN-ROW-VALIDITY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7917963Z   [must] requirement_quality REQ-NOTIF-DRAIN-ROW-VALIDITY criterion=length — title is 288 words; want 3..=25
2026-09-06T13:09:01.7918240Z   [must] requirement_quality REQ-NOTIF-MIGRATE criterion=length — title is 46 words; want 3..=25
2026-09-06T13:09:01.7918535Z   [must] requirement_quality REQ-NOTIF-QUIET-DELIVERY criterion=length — title is 55 words; want 3..=25
2026-09-06T13:09:01.7918907Z   [must] requirement_quality REQ-NOTIF-SCOPE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7919260Z   [must] requirement_quality REQ-NOTIF-SCOPE criterion=length — title is 65 words; want 3..=25
2026-09-06T13:09:01.7919660Z   [must] requirement_quality REQ-NOTIF-SEAM-DISMISS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7919957Z   [must] requirement_quality REQ-NOTIF-SEAM-DISMISS criterion=length — title is 56 words; want 3..=25
2026-09-06T13:09:01.7920220Z   [must] requirement_quality REQ-NOTIF-TTL criterion=length — title is 44 words; want 3..=25
2026-09-06T13:09:01.7920653Z   [must] requirement_quality REQ-NOTIF-UPDATE-ROW-VERSION-RETIRE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7920996Z   [must] requirement_quality REQ-NOTIF-UPDATE-ROW-VERSION-RETIRE criterion=length — title is 450 words; want 3..=25
2026-09-06T13:09:01.7921417Z   [must] requirement_quality REQ-NOW-SIGNAL-CATEGORIES-V1 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7921737Z   [must] requirement_quality REQ-NOW-SIGNAL-CATEGORIES-V1 criterion=length — title is 205 words; want 3..=25
2026-09-06T13:09:01.7922179Z   [must] requirement_quality REQ-NOW-SIGNAL-DEFERRED-CATEGORIES criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7922510Z   [must] requirement_quality REQ-NOW-SIGNAL-DEFERRED-CATEGORIES criterion=length — title is 80 words; want 3..=25
2026-09-06T13:09:01.7922890Z   [must] requirement_quality REQ-NOW-SIGNAL-DELTA criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7923159Z   [must] requirement_quality REQ-NOW-SIGNAL-DELTA criterion=length — title is 202 words; want 3..=25
2026-09-06T13:09:01.7923707Z   [must] requirement_quality REQ-NOW-SIGNAL-EDGE-SUBJECT-NAMING criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7924041Z   [must] requirement_quality REQ-NOW-SIGNAL-EDGE-SUBJECT-NAMING criterion=length — title is 190 words; want 3..=25
2026-09-06T13:09:01.7924454Z   [must] requirement_quality REQ-NOW-SIGNAL-SEAL-BRIEF criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7924846Z   [must] requirement_quality REQ-NOW-SIGNAL-SEAL-BRIEF criterion=length — title is 206 words; want 3..=25
2026-09-06T13:09:01.7925221Z   [must] requirement_quality REQ-NOW-SIGNAL-SPEC criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7925507Z   [must] requirement_quality REQ-NOW-SIGNAL-SPEC criterion=length — title is 146 words; want 3..=25
2026-09-06T13:09:01.7925899Z   [must] requirement_quality REQ-NOW-SIGNAL-UPDATES criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7926204Z   [must] requirement_quality REQ-NOW-SIGNAL-UPDATES criterion=length — title is 285 words; want 3..=25
2026-09-06T13:09:01.7926582Z   [must] requirement_quality REQ-NOW-SIGNAL-VERB criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7926858Z   [must] requirement_quality REQ-NOW-SIGNAL-VERB criterion=length — title is 182 words; want 3..=25
2026-09-06T13:09:01.7927226Z   [must] requirement_quality REQ-ONEWAY-STREAM-TERMINAL criterion=length — title is 102 words; want 3..=25
2026-09-06T13:09:01.7927627Z   [must] requirement_quality REQ-OPID-MINTER-NAMESPACE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7927932Z   [must] requirement_quality REQ-OPID-MINTER-NAMESPACE criterion=length — title is 337 words; want 3..=25
2026-09-06T13:09:01.7928317Z   [must] requirement_quality REQ-OPID-TRACING-RETRY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7928622Z   [must] requirement_quality REQ-OPID-TRACING-RETRY criterion=length — title is 222 words; want 3..=25
2026-09-06T13:09:01.7929066Z   [must] requirement_quality REQ-PAIR-8 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7929324Z   [must] requirement_quality REQ-PAIR-8 criterion=length — title is 67 words; want 3..=25
2026-09-06T13:09:01.7929715Z   [must] requirement_quality REQ-PAIR-NTP-LOUD-FAIL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7930001Z   [must] requirement_quality REQ-PAIR-NTP-LOUD-FAIL criterion=length — title is 104 words; want 3..=25
2026-09-06T13:09:01.7930386Z   [must] requirement_quality REQ-PAIR-NTP-MULTIHOME criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7930674Z   [must] requirement_quality REQ-PAIR-NTP-MULTIHOME criterion=length — title is 131 words; want 3..=25
2026-09-06T13:09:01.7931080Z   [must] requirement_quality REQ-PEER-ABSENCE-VERDICT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7931376Z   [must] requirement_quality REQ-PEER-ABSENCE-VERDICT criterion=length — title is 453 words; want 3..=25
2026-09-06T13:09:01.7931790Z   [must] requirement_quality REQ-PEER-COUNT-TARGET-SCOPED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7932172Z   [must] requirement_quality REQ-PEER-COUNT-TARGET-SCOPED criterion=length — title is 340 words; want 3..=25
2026-09-06T13:09:01.7932592Z   [must] requirement_quality REQ-PEER-HEALTH-PARTIAL-DEGRADE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7932924Z   [must] requirement_quality REQ-PEER-HEALTH-PARTIAL-DEGRADE criterion=length — title is 272 words; want 3..=25
2026-09-06T13:09:01.7933318Z   [must] requirement_quality REQ-PEER-PUMP-CHURN-STALL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7933719Z   [must] requirement_quality REQ-PEER-PUMP-CHURN-STALL criterion=length — title is 97 words; want 3..=25
2026-09-06T13:09:01.7934099Z   [must] requirement_quality REQ-PEER-ROUTE-CHAIN criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7934391Z   [must] requirement_quality REQ-PEER-ROUTE-CHAIN criterion=length — title is 177 words; want 3..=25
2026-09-06T13:09:01.7934786Z   [must] requirement_quality REQ-PEERADDR-INVARIANT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7935077Z   [must] requirement_quality REQ-PEERADDR-INVARIANT criterion=length — title is 138 words; want 3..=25
2026-09-06T13:09:01.7935484Z   [must] requirement_quality REQ-PERCH-GC-REFUSAL-REPORT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7935796Z   [must] requirement_quality REQ-PERCH-GC-REFUSAL-REPORT criterion=length — title is 192 words; want 3..=25
2026-09-06T13:09:01.7936225Z   [must] requirement_quality REQ-PERCH-GC-RESIDUE-PREDICATE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7936549Z   [must] requirement_quality REQ-PERCH-GC-RESIDUE-PREDICATE criterion=length — title is 388 words; want 3..=25
2026-09-06T13:09:01.7936987Z   [must] requirement_quality REQ-PICK-FIDELITY-AUDIT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7937288Z   [must] requirement_quality REQ-PICK-FIDELITY-AUDIT criterion=length — title is 416 words; want 3..=25
2026-09-06T13:09:01.7937655Z   [must] requirement_quality REQ-PICKER-1 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7937908Z   [must] requirement_quality REQ-PICKER-1 criterion=length — title is 156 words; want 3..=25
2026-09-06T13:09:01.7938173Z   [must] requirement_quality REQ-PICKER-2 criterion=length — title is 77 words; want 3..=25
2026-09-06T13:09:01.7938435Z   [must] requirement_quality REQ-PICKER-3 criterion=length — title is 120 words; want 3..=25
2026-09-06T13:09:01.7938792Z   [must] requirement_quality REQ-PICKER-4 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7939135Z   [must] requirement_quality REQ-PICKER-4 criterion=length — title is 84 words; want 3..=25
2026-09-06T13:09:01.7939493Z   [must] requirement_quality REQ-PICKER-5 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7939745Z   [must] requirement_quality REQ-PICKER-5 criterion=length — title is 147 words; want 3..=25
2026-09-06T13:09:01.7940165Z   [must] requirement_quality REQ-PICKER-ADAPTER-DESCRIPTION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7940494Z   [must] requirement_quality REQ-PICKER-ADAPTER-DESCRIPTION criterion=length — title is 64 words; want 3..=25
2026-09-06T13:09:01.7940888Z   [must] requirement_quality REQ-PICKER-BACK-NAV criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7941167Z   [must] requirement_quality REQ-PICKER-BACK-NAV criterion=length — title is 140 words; want 3..=25
2026-09-06T13:09:01.7941583Z   [must] requirement_quality REQ-PICKER-CHANGE-ADAPTER-FLOW criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7941955Z   [must] requirement_quality REQ-PICKER-CHANGE-ADAPTER-FLOW criterion=length — title is 117 words; want 3..=25
2026-09-06T13:09:01.7942360Z   [must] requirement_quality REQ-PICKER-CHOOSE-DEDUP-ALL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7942665Z   [must] requirement_quality REQ-PICKER-CHOOSE-DEDUP-ALL criterion=length — title is 72 words; want 3..=25
2026-09-06T13:09:01.7942998Z   [must] requirement_quality REQ-PICKER-CONTROL-LINE-STATUS-GATE criterion=length — title is 71 words; want 3..=25
2026-09-06T13:09:01.7943493Z   [must] requirement_quality REQ-PICKER-CONTROLLED-LOCAL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7943796Z   [must] requirement_quality REQ-PICKER-CONTROLLED-LOCAL criterion=length — title is 129 words; want 3..=25
2026-09-06T13:09:01.7944116Z   [must] requirement_quality REQ-PICKER-CURRENT-DIR-LABEL criterion=length — title is 114 words; want 3..=25
2026-09-06T13:09:01.7944565Z   [must] requirement_quality REQ-PICKER-FORK-LABEL-CWD criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7944855Z   [must] requirement_quality REQ-PICKER-FORK-LABEL-CWD criterion=length — title is 58 words; want 3..=25
2026-09-06T13:09:01.7945147Z   [must] requirement_quality REQ-PICKER-HISTORY-FRESH criterion=length — title is 51 words; want 3..=25
2026-09-06T13:09:01.7945476Z   [must] requirement_quality REQ-PICKER-HISTORY-FRESH criterion=tbd-todo — title contains placeholder marker 'TBD'
2026-09-06T13:09:01.7945819Z   [must] requirement_quality REQ-PICKER-KEY-GATE-LAUNCH-CAPABLE criterion=length — title is 89 words; want 3..=25
2026-09-06T13:09:01.7946204Z   [must] requirement_quality REQ-PICKER-LIST-SCROLL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7946496Z   [must] requirement_quality REQ-PICKER-LIST-SCROLL criterion=length — title is 547 words; want 3..=25
2026-09-06T13:09:01.7946804Z   [must] requirement_quality REQ-PICKER-NODE-GROUPING criterion=length — title is 73 words; want 3..=25
2026-09-06T13:09:01.7947110Z   [must] requirement_quality REQ-PICKER-OFFLINE-NO-VIEW criterion=length — title is 46 words; want 3..=25
2026-09-06T13:09:01.7947401Z   [must] requirement_quality REQ-PICKER-ONLINE-ACTION criterion=length — title is 74 words; want 3..=25
2026-09-06T13:09:01.7947735Z   [must] requirement_quality REQ-PICKER-ONLINE-ACTION criterion=tbd-todo — title contains placeholder marker 'TBD'
2026-09-06T13:09:01.7948064Z   [must] requirement_quality REQ-PICKER-PROJECT-DISPLAY-NAME criterion=length — title is 103 words; want 3..=25
2026-09-06T13:09:01.7948388Z   [must] requirement_quality REQ-PICKER-PROJECT-HISTORY-TRUTH criterion=length — title is 128 words; want 3..=25
2026-09-06T13:09:01.7948780Z   [must] requirement_quality REQ-PICKER-PURGE-SHORTCUT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7949175Z   [must] requirement_quality REQ-PICKER-PURGE-SHORTCUT criterion=length — title is 180 words; want 3..=25
2026-09-06T13:09:01.7949581Z   [must] requirement_quality REQ-PICKER-PURGE-STRUCTURED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7949887Z   [must] requirement_quality REQ-PICKER-PURGE-STRUCTURED criterion=length — title is 151 words; want 3..=25
2026-09-06T13:09:01.7950273Z   [must] requirement_quality REQ-PICKER-REMOTE-WAKE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7950569Z   [must] requirement_quality REQ-PICKER-REMOTE-WAKE criterion=length — title is 296 words; want 3..=25
2026-09-06T13:09:01.7950987Z   [must] requirement_quality REQ-PICKER-RESUME-CONTEXT-PANEL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7951368Z   [must] requirement_quality REQ-PICKER-RESUME-CONTEXT-PANEL criterion=length — title is 53 words; want 3..=25
2026-09-06T13:09:01.7951800Z   [must] requirement_quality REQ-PICKER-SHORTCUT-LABEL-FILENAME criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7952133Z   [must] requirement_quality REQ-PICKER-SHORTCUT-LABEL-FILENAME criterion=length — title is 75 words; want 3..=25
2026-09-06T13:09:01.7952444Z   [must] requirement_quality REQ-PICKER-START-PROJECT-CHOICE criterion=length — title is 86 words; want 3..=25
2026-09-06T13:09:01.7952821Z   [must] requirement_quality REQ-PICKER-UX-V013 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7953187Z   [must] requirement_quality REQ-PICKER-UX-V013 criterion=length — title is 86 words; want 3..=25
2026-09-06T13:09:01.7953474Z   [must] requirement_quality REQ-PICKER-WINDOW-TITLE criterion=length — title is 68 words; want 3..=25
2026-09-06T13:09:01.7953869Z   [must] requirement_quality REQ-PID-ROLE-EVIDENCE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7954223Z   [must] requirement_quality REQ-PID-ROLE-EVIDENCE criterion=length — title is 503 words; want 3..=25
2026-09-06T13:09:01.7954595Z   [must] requirement_quality REQ-PLATFORM-MUSL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7954867Z   [must] requirement_quality REQ-PLATFORM-MUSL criterion=length — title is 106 words; want 3..=25
2026-09-06T13:09:01.7955257Z   [must] requirement_quality REQ-PLATFORM-REGISTRY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7955558Z   [must] requirement_quality REQ-PLATFORM-REGISTRY criterion=length — title is 126 words; want 3..=25
2026-09-06T13:09:01.7955945Z   [must] requirement_quality REQ-POOL-CLAIM-TREE-MATCH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7956265Z   [must] requirement_quality REQ-POOL-CLAIM-TREE-MATCH criterion=length — title is 143 words; want 3..=25
2026-09-06T13:09:01.7956685Z   [must] requirement_quality REQ-POOL-FOREIGN-TREE-REFUSED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7957022Z   [must] requirement_quality REQ-POOL-FOREIGN-TREE-REFUSED criterion=length — title is 427 words; want 3..=25
2026-09-06T13:09:01.7957427Z   [must] requirement_quality REQ-POOL-GC-ORPHAN-RECLAIM criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7957756Z   [must] requirement_quality REQ-POOL-GC-ORPHAN-RECLAIM criterion=length — title is 382 words; want 3..=25
2026-09-06T13:09:01.7958204Z   [must] requirement_quality REQ-POOL-GUARD-NOT-SILENCEABLE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7958534Z   [must] requirement_quality REQ-POOL-GUARD-NOT-SILENCEABLE criterion=length — title is 222 words; want 3..=25
2026-09-06T13:09:01.7958929Z   [must] requirement_quality REQ-POOL-LANE-IDENTITY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7959312Z   [must] requirement_quality REQ-POOL-LANE-IDENTITY criterion=length — title is 299 words; want 3..=25
2026-09-06T13:09:01.7959735Z   [must] requirement_quality REQ-POOL-ONE-LIVENESS-PREDICATE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7960064Z   [must] requirement_quality REQ-POOL-ONE-LIVENESS-PREDICATE criterion=length — title is 127 words; want 3..=25
2026-09-06T13:09:01.7960427Z   [must] requirement_quality REQ-PRES-1 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7960679Z   [must] requirement_quality REQ-PRES-1 criterion=length — title is 48 words; want 3..=25
2026-09-06T13:09:01.7961110Z   [must] requirement_quality REQ-PRESENCE-CONTROL-REAP-ON-EXIT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7961515Z   [must] requirement_quality REQ-PRESENCE-CONTROL-REAP-ON-EXIT criterion=length — title is 139 words; want 3..=25
2026-09-06T13:09:01.7961825Z   [must] requirement_quality REQ-PRESENCE-LIVENESS-TRUTH criterion=length — title is 215 words; want 3..=25
2026-09-06T13:09:01.7962152Z   [must] requirement_quality REQ-PROJECT-INDEX-INVALIDATION criterion=length — title is 124 words; want 3..=25
2026-09-06T13:09:01.7962583Z   [must] requirement_quality REQ-PROJECT-INDEX-READER-CUTOVER criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7963013Z   [must] requirement_quality REQ-PROJECT-INDEX-READER-CUTOVER criterion=length — title is 143 words; want 3..=25
2026-09-06T13:09:01.7963309Z   [must] requirement_quality REQ-PROJECT-INDEX-STORE criterion=length — title is 90 words; want 3..=25
2026-09-06T13:09:01.7963604Z   [must] requirement_quality REQ-PROJECT-INDEX-WRITER criterion=length — title is 151 words; want 3..=25
2026-09-06T13:09:01.7964086Z   [must] requirement_quality REQ-PSYCHE-ACCOUNT-REFUSAL-EXIT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7964410Z   [must] requirement_quality REQ-PSYCHE-ACCOUNT-REFUSAL-EXIT criterion=length — title is 321 words; want 3..=25
2026-09-06T13:09:01.7964859Z   [must] requirement_quality REQ-PSYCHE-CONTEXT-FILE-INDIRECTION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7965202Z   [must] requirement_quality REQ-PSYCHE-CONTEXT-FILE-INDIRECTION criterion=length — title is 206 words; want 3..=25
2026-09-06T13:09:01.7965661Z   [must] requirement_quality REQ-PSYCHE-CRASHLOOP-BACKOFF-SHUTDOWN criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7966013Z   [must] requirement_quality REQ-PSYCHE-CRASHLOOP-BACKOFF-SHUTDOWN criterion=length — title is 90 words; want 3..=25
2026-09-06T13:09:01.7966457Z   [must] requirement_quality REQ-PSYCHE-EPHEMERAL-DRIVER criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7966805Z   [must] requirement_quality REQ-PSYCHE-EPHEMERAL-DRIVER criterion=length — title is 146 words; want 3..=25
2026-09-06T13:09:01.7967268Z   [must] requirement_quality REQ-PSYCHE-FAULT-SLOTS-SURVIVE-RESTART criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7967616Z   [must] requirement_quality REQ-PSYCHE-FAULT-SLOTS-SURVIVE-RESTART criterion=length — title is 586 words; want 3..=25
2026-09-06T13:09:01.7968053Z   [must] requirement_quality REQ-PSYCHE-HARD-BUDGET-PER-KIND criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7968377Z   [must] requirement_quality REQ-PSYCHE-HARD-BUDGET-PER-KIND criterion=length — title is 441 words; want 3..=25
2026-09-06T13:09:01.7968798Z   [must] requirement_quality REQ-PSYCHE-INGEST-FAILURE-LOUD criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7969222Z   [must] requirement_quality REQ-PSYCHE-INGEST-FAILURE-LOUD criterion=length — title is 631 words; want 3..=25
2026-09-06T13:09:01.7969659Z   [must] requirement_quality REQ-PSYCHE-INGEST-FAULT-BRIEF-WARN criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7969995Z   [must] requirement_quality REQ-PSYCHE-INGEST-FAULT-BRIEF-WARN criterion=length — title is 296 words; want 3..=25
2026-09-06T13:09:01.7970420Z   [must] requirement_quality REQ-PSYCHE-INGEST-INTENT-MARKER criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7970759Z   [must] requirement_quality REQ-PSYCHE-INGEST-INTENT-MARKER criterion=length — title is 372 words; want 3..=25
2026-09-06T13:09:01.7971183Z   [must] requirement_quality REQ-PSYCHE-INGEST-SERIALIZED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7971578Z   [must] requirement_quality REQ-PSYCHE-INGEST-SERIALIZED criterion=length — title is 556 words; want 3..=25
2026-09-06T13:09:01.7972028Z   [must] requirement_quality REQ-PSYCHE-INVOCATION-BUDGET-PER-ROLE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7972376Z   [must] requirement_quality REQ-PSYCHE-INVOCATION-BUDGET-PER-ROLE criterion=length — title is 341 words; want 3..=25
2026-09-06T13:09:01.7972805Z   [must] requirement_quality REQ-PSYCHE-LEGACY-RESIDENT-SWEEP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7973234Z   [must] requirement_quality REQ-PSYCHE-LEGACY-RESIDENT-SWEEP criterion=length — title is 282 words; want 3..=25
2026-09-06T13:09:01.7973646Z   [must] requirement_quality REQ-PSYCHE-NESTED-RESOLUTION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7973965Z   [must] requirement_quality REQ-PSYCHE-NESTED-RESOLUTION criterion=length — title is 147 words; want 3..=25
2026-09-06T13:09:01.7974450Z   [must] requirement_quality REQ-PSYCHE-OUTCOME-CLASSIFIED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7974770Z   [must] requirement_quality REQ-PSYCHE-OUTCOME-CLASSIFIED criterion=length — title is 260 words; want 3..=25
2026-09-06T13:09:01.7975183Z   [must] requirement_quality REQ-PSYCHE-ROLE-ABSENT-STATUS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7975501Z   [must] requirement_quality REQ-PSYCHE-ROLE-ABSENT-STATUS criterion=length — title is 333 words; want 3..=25
2026-09-06T13:09:01.7975925Z   [must] requirement_quality REQ-PSYCHE-ROLE-OPTIONAL-SKIP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7976245Z   [must] requirement_quality REQ-PSYCHE-ROLE-OPTIONAL-SKIP criterion=length — title is 125 words; want 3..=25
2026-09-06T13:09:01.7976636Z   [must] requirement_quality REQ-PSYCHE-SID-CUSTODY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7976936Z   [must] requirement_quality REQ-PSYCHE-SID-CUSTODY criterion=length — title is 134 words; want 3..=25
2026-09-06T13:09:01.7977356Z   [must] requirement_quality REQ-PSYCHE-SOFT-TIMEOUT-BUDGET criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7977675Z   [must] requirement_quality REQ-PSYCHE-SOFT-TIMEOUT-BUDGET criterion=length — title is 250 words; want 3..=25
2026-09-06T13:09:01.7978094Z   [must] requirement_quality REQ-PSYCHE-SPAWN-ENV-PARITY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7978409Z   [must] requirement_quality REQ-PSYCHE-SPAWN-ENV-PARITY criterion=length — title is 167 words; want 3..=25
2026-09-06T13:09:01.7978848Z   [must] requirement_quality REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7979285Z   [must] requirement_quality REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS criterion=length — title is 276 words; want 3..=25
2026-09-06T13:09:01.7979711Z   [must] requirement_quality REQ-PSYCHE-TURN-STREAM-EVIDENCE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7980035Z   [must] requirement_quality REQ-PSYCHE-TURN-STREAM-EVIDENCE criterion=length — title is 156 words; want 3..=25
2026-09-06T13:09:01.7980437Z   [must] requirement_quality REQ-PUBLIC-ERROR-SURFACES criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7980747Z   [must] requirement_quality REQ-PUBLIC-ERROR-SURFACES criterion=length — title is 387 words; want 3..=25
2026-09-06T13:09:01.7981137Z   [must] requirement_quality REQ-PUMP-DIAL-FASTFAIL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7981490Z   [must] requirement_quality REQ-PUMP-DIAL-FASTFAIL criterion=length — title is 324 words; want 3..=25
2026-09-06T13:09:01.7981891Z   [must] requirement_quality REQ-PUMP-PEER-ISOLATION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7982187Z   [must] requirement_quality REQ-PUMP-PEER-ISOLATION criterion=length — title is 199 words; want 3..=25
2026-09-06T13:09:01.7982574Z   [must] requirement_quality REQ-PUMP-STAGE-TRUTH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7982860Z   [must] requirement_quality REQ-PUMP-STAGE-TRUTH criterion=length — title is 168 words; want 3..=25
2026-09-06T13:09:01.7983206Z   [must] requirement_quality REQ-RC-1 criterion=length — title is 94 words; want 3..=25
2026-09-06T13:09:01.7983609Z   [must] requirement_quality REQ-RC-CROSS-NODE-ATTACH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7983905Z   [must] requirement_quality REQ-RC-CROSS-NODE-ATTACH criterion=length — title is 95 words; want 3..=25
2026-09-06T13:09:01.7984371Z   [must] requirement_quality REQ-RC-DISPLAY-SOLE-WRITER criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7984681Z   [must] requirement_quality REQ-RC-DISPLAY-SOLE-WRITER criterion=length — title is 351 words; want 3..=25
2026-09-06T13:09:01.7985088Z   [must] requirement_quality REQ-RC-DRIVER-READ-LIVENESS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7985403Z   [must] requirement_quality REQ-RC-DRIVER-READ-LIVENESS criterion=length — title is 446 words; want 3..=25
2026-09-06T13:09:01.7985815Z   [must] requirement_quality REQ-RC-HARNESS-ONLY-REFUSAL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7986124Z   [must] requirement_quality REQ-RC-HARNESS-ONLY-REFUSAL criterion=length — title is 129 words; want 3..=25
2026-09-06T13:09:01.7986452Z   [must] requirement_quality REQ-RC-HONEST-SESSION-AUTHORITY criterion=length — title is 144 words; want 3..=25
2026-09-06T13:09:01.7986829Z   [must] requirement_quality REQ-RC-IDENTITY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7987101Z   [must] requirement_quality REQ-RC-IDENTITY criterion=length — title is 122 words; want 3..=25
2026-09-06T13:09:01.7987487Z   [must] requirement_quality REQ-RC-IDMARKER-DISABLE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7987782Z   [must] requirement_quality REQ-RC-IDMARKER-DISABLE criterion=length — title is 73 words; want 3..=25
2026-09-06T13:09:01.7988178Z   [must] requirement_quality REQ-RC-KEY-VT-TRANSLATE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7988477Z   [must] requirement_quality REQ-RC-KEY-VT-TRANSLATE criterion=length — title is 249 words; want 3..=25
2026-09-06T13:09:01.7988863Z   [must] requirement_quality REQ-RC-MOUSE-FORWARD criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7989236Z   [must] requirement_quality REQ-RC-MOUSE-FORWARD criterion=length — title is 218 words; want 3..=25
2026-09-06T13:09:01.7989679Z   [must] requirement_quality REQ-RC-NEWLINE-PRESENTATION-TRUTH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7990013Z   [must] requirement_quality REQ-RC-NEWLINE-PRESENTATION-TRUTH criterion=length — title is 174 words; want 3..=25
2026-09-06T13:09:01.7990337Z   [must] requirement_quality REQ-RC-QUALIFIED-TARGET-CANONICAL criterion=length — title is 117 words; want 3..=25
2026-09-06T13:09:01.7990718Z   [must] requirement_quality REQ-RC-RECONNECT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7990988Z   [must] requirement_quality REQ-RC-RECONNECT criterion=length — title is 244 words; want 3..=25
2026-09-06T13:09:01.7991439Z   [must] requirement_quality REQ-RC-RECONNECT-TRUTH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7991740Z   [must] requirement_quality REQ-RC-RECONNECT-TRUTH criterion=length — title is 84 words; want 3..=25
2026-09-06T13:09:01.7992147Z   [must] requirement_quality REQ-RC-RESIZE-GEOMETRY-EPOCH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7992466Z   [must] requirement_quality REQ-RC-RESIZE-GEOMETRY-EPOCH criterion=length — title is 1038 words; want 3..=25
2026-09-06T13:09:01.7992899Z   [must] requirement_quality REQ-RC-RESIZE-PRESENTATION-BARRIER criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7993362Z   [must] requirement_quality REQ-RC-RESIZE-PRESENTATION-BARRIER criterion=length — title is 1140 words; want 3..=25
2026-09-06T13:09:01.7993766Z   [must] requirement_quality REQ-RC-SINGLE-PUMP-BRAIN criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7994117Z   [must] requirement_quality REQ-RC-SINGLE-PUMP-BRAIN criterion=length — title is 171 words; want 3..=25
2026-09-06T13:09:01.7994498Z   [must] requirement_quality REQ-RC-VT-TEARDOWN criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7994770Z   [must] requirement_quality REQ-RC-VT-TEARDOWN criterion=length — title is 149 words; want 3..=25
2026-09-06T13:09:01.7995142Z   [must] requirement_quality REQ-RC-WIN-PASTE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7995428Z   [must] requirement_quality REQ-RC-WIN-PASTE criterion=length — title is 226 words; want 3..=25
2026-09-06T13:09:01.7995809Z   [must] requirement_quality REQ-RC-WIN-VT-OUTPUT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7996091Z   [must] requirement_quality REQ-RC-WIN-VT-OUTPUT criterion=length — title is 150 words; want 3..=25
2026-09-06T13:09:01.7996450Z   [must] requirement_quality REQ-RCVIEW-1 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7996717Z   [must] requirement_quality REQ-RCVIEW-1 criterion=length — title is 197 words; want 3..=25
2026-09-06T13:09:01.7997203Z   [must] requirement_quality REQ-READY-AGENT-RESUME criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7997493Z   [must] requirement_quality REQ-READY-AGENT-RESUME criterion=length — title is 164 words; want 3..=25
2026-09-06T13:09:01.7997938Z   [must] requirement_quality REQ-RECONCILE-FAILED-ADDR-REFUSE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7998272Z   [must] requirement_quality REQ-RECONCILE-FAILED-ADDR-REFUSE criterion=length — title is 154 words; want 3..=25
2026-09-06T13:09:01.7998601Z   [must] requirement_quality REQ-REDISPATCH-FINISHED-RETIRE criterion=length — title is 125 words; want 3..=25
2026-09-06T13:09:01.7999132Z   [must] requirement_quality REQ-REGISTRY-APPLY-TRANSACTIONAL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.7999461Z   [must] requirement_quality REQ-REGISTRY-APPLY-TRANSACTIONAL criterion=length — title is 129 words; want 3..=25
2026-09-06T13:09:01.7999776Z   [must] requirement_quality REQ-REGISTRY-REPLAY-BOUNDED criterion=length — title is 125 words; want 3..=25
2026-09-06T13:09:01.8000211Z   [must] requirement_quality REQ-REL-1 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8000469Z   [must] requirement_quality REQ-REL-1 criterion=length — title is 61 words; want 3..=25
2026-09-06T13:09:01.8000888Z   [must] requirement_quality REQ-RELAY-DEATH-CONVERGENCE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8001203Z   [must] requirement_quality REQ-RELAY-DEATH-CONVERGENCE criterion=length — title is 304 words; want 3..=25
2026-09-06T13:09:01.8001585Z   [must] requirement_quality REQ-RELAY-NO-BUSY-DELIVER criterion=length — title is 159 words; want 3..=25
2026-09-06T13:09:01.8001886Z   [must] requirement_quality REQ-RELEASE-CHANNEL-PRIVATE criterion=length — title is 97 words; want 3..=25
2026-09-06T13:09:01.8002182Z   [must] requirement_quality REQ-RELEASE-MUSL-ARTIFACT criterion=length — title is 114 words; want 3..=25
2026-09-06T13:09:01.8002596Z   [must] requirement_quality REQ-RELEASE-VERSION-TAG-ASSERT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8003007Z   [must] requirement_quality REQ-RELEASE-VERSION-TAG-ASSERT criterion=length — title is 348 words; want 3..=25
2026-09-06T13:09:01.8003393Z   [must] requirement_quality REQ-RESIDENT-SERVICE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8003675Z   [must] requirement_quality REQ-RESIDENT-SERVICE criterion=length — title is 3453 words; want 3..=25
2026-09-06T13:09:01.8004150Z   [must] requirement_quality REQ-RESIZE-INPUT-MODE-INTEGRITY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8004476Z   [must] requirement_quality REQ-RESIZE-INPUT-MODE-INTEGRITY criterion=length — title is 465 words; want 3..=25
2026-09-06T13:09:01.8004876Z   [must] requirement_quality REQ-REST-TERMINAL-NORMALIZE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8005191Z   [must] requirement_quality REQ-REST-TERMINAL-NORMALIZE criterion=length — title is 130 words; want 3..=25
2026-09-06T13:09:01.8005578Z   [must] requirement_quality REQ-REST-VERB-ROUTING criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8005869Z   [must] requirement_quality REQ-REST-VERB-ROUTING criterion=length — title is 326 words; want 3..=25
2026-09-06T13:09:01.8006303Z   [must] requirement_quality REQ-RESUME-ADAPTER-FOLLOWS-SESSION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8006674Z   [must] requirement_quality REQ-RESUME-ADAPTER-FOLLOWS-SESSION criterion=length — title is 275 words; want 3..=25
2026-09-06T13:09:01.8007080Z   [must] requirement_quality REQ-RESUME-CONTEXT-PULL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8007377Z   [must] requirement_quality REQ-RESUME-CONTEXT-PULL criterion=length — title is 460 words; want 3..=25
2026-09-06T13:09:01.8007801Z   [must] requirement_quality REQ-RESUME-CUSTODY-IDENTITY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8008155Z   [must] requirement_quality REQ-RESUME-CUSTODY-IDENTITY criterion=length — title is 206 words; want 3..=25
2026-09-06T13:09:01.8008474Z   [must] requirement_quality REQ-RESUME-HARNESS-SESSION-ID criterion=length — title is 128 words; want 3..=25
2026-09-06T13:09:01.8008799Z   [must] requirement_quality REQ-RESUME-REAP-PRIOR-HARNESS criterion=length — title is 54 words; want 3..=25
2026-09-06T13:09:01.8009189Z   [must] requirement_quality REQ-RESUME-ROW-PER-PROJECT criterion=length — title is 59 words; want 3..=25
2026-09-06T13:09:01.8009593Z   [must] requirement_quality REQ-RESUME-UNBOUND-STAMP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8009903Z   [must] requirement_quality REQ-RESUME-UNBOUND-STAMP criterion=length — title is 153 words; want 3..=25
2026-09-06T13:09:01.8010265Z   [must] requirement_quality REQ-RING-BLOCKS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8010651Z   [must] requirement_quality REQ-RING-BLOCKS criterion=length — title is 826 words; want 3..=25
2026-09-06T13:09:01.8011101Z   [must] requirement_quality REQ-RING-TIMEOUT-MINUTES criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8011453Z   [must] requirement_quality REQ-RING-TIMEOUT-MINUTES criterion=length — title is 387 words; want 3..=25
2026-09-06T13:09:01.8011862Z   [must] requirement_quality REQ-ROSTER-SELF-LEASE-HEAL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8012169Z   [must] requirement_quality REQ-ROSTER-SELF-LEASE-HEAL criterion=length — title is 320 words; want 3..=25
2026-09-06T13:09:01.8012598Z   [must] requirement_quality REQ-ROUTE-CHAIN-FAILED-ADDR-SKIP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8012923Z   [must] requirement_quality REQ-ROUTE-CHAIN-FAILED-ADDR-SKIP criterion=length — title is 144 words; want 3..=25
2026-09-06T13:09:01.8013218Z   [must] requirement_quality REQ-RUN-EMPTY-CREATE criterion=length — title is 63 words; want 3..=25
2026-09-06T13:09:01.8013625Z   [must] requirement_quality REQ-RUN-ID-REUSES-ADAPTER criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8013982Z   [must] requirement_quality REQ-RUN-ID-REUSES-ADAPTER criterion=length — title is 174 words; want 3..=25
2026-09-06T13:09:01.8014381Z   [must] requirement_quality REQ-RUN-MULTISUBNET-HOME criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8014681Z   [must] requirement_quality REQ-RUN-MULTISUBNET-HOME criterion=length — title is 120 words; want 3..=25
2026-09-06T13:09:01.8015070Z   [must] requirement_quality REQ-RUN-NO-DUP-SESSION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8015370Z   [must] requirement_quality REQ-RUN-NO-DUP-SESSION criterion=length — title is 137 words; want 3..=25
2026-09-06T13:09:01.8015732Z   [must] requirement_quality REQ-RUN-PICKER criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8016004Z   [must] requirement_quality REQ-RUN-PICKER criterion=length — title is 203 words; want 3..=25
2026-09-06T13:09:01.8016395Z   [must] requirement_quality REQ-RUN-PICKER-HOME criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8016677Z   [must] requirement_quality REQ-RUN-PICKER-HOME criterion=length — title is 154 words; want 3..=25
2026-09-06T13:09:01.8017049Z   [must] requirement_quality REQ-RUN-SHORTCUT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8017319Z   [must] requirement_quality REQ-RUN-SHORTCUT criterion=length — title is 232 words; want 3..=25
2026-09-06T13:09:01.8017766Z   [must] requirement_quality REQ-SCREENGRID-REPAINT-MODE-REPLAY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8018114Z   [must] requirement_quality REQ-SCREENGRID-REPAINT-MODE-REPLAY criterion=length — title is 100 words; want 3..=25
2026-09-06T13:09:01.8018500Z   [must] requirement_quality REQ-SCREENGRID-WIDTH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8018800Z   [must] requirement_quality REQ-SCREENGRID-WIDTH criterion=length — title is 254 words; want 3..=25
2026-09-06T13:09:01.8019291Z   [must] requirement_quality REQ-SEAL-AUTH-BACKEND-SEAM criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8019601Z   [must] requirement_quality REQ-SEAL-AUTH-BACKEND-SEAM criterion=length — title is 535 words; want 3..=25
2026-09-06T13:09:01.8020031Z   [must] requirement_quality REQ-SEAL-CEREMONY-CONTENT-SHOWN criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8020459Z   [must] requirement_quality REQ-SEAL-CEREMONY-CONTENT-SHOWN criterion=length — title is 310 words; want 3..=25
2026-09-06T13:09:01.8020873Z   [must] requirement_quality REQ-SEAL-CEREMONY-ESC-CANCEL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8021187Z   [must] requirement_quality REQ-SEAL-CEREMONY-ESC-CANCEL criterion=length — title is 190 words; want 3..=25
2026-09-06T13:09:01.8021637Z   [must] requirement_quality REQ-SEAL-CEREMONY-FIDO2 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8021933Z   [must] requirement_quality REQ-SEAL-CEREMONY-FIDO2 criterion=length — title is 417 words; want 3..=25
2026-09-06T13:09:01.8022328Z   [must] requirement_quality REQ-SEAL-CEREMONY-RC-CLIENT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8022641Z   [must] requirement_quality REQ-SEAL-CEREMONY-RC-CLIENT criterion=length — title is 206 words; want 3..=25
2026-09-06T13:09:01.8023030Z   [must] requirement_quality REQ-SEAL-CEREMONY-TOTP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8023317Z   [must] requirement_quality REQ-SEAL-CEREMONY-TOTP criterion=length — title is 436 words; want 3..=25
2026-09-06T13:09:01.8023678Z   [must] requirement_quality REQ-SEAL-DESCRIBE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8024007Z   [must] requirement_quality REQ-SEAL-DESCRIBE criterion=length — title is 470 words; want 3..=25
2026-09-06T13:09:01.8024452Z   [must] requirement_quality REQ-SEAL-ENROLL-RECORD-SUBNET-MATERIAL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8024795Z   [must] requirement_quality REQ-SEAL-ENROLL-RECORD-SUBNET-MATERIAL criterion=length — title is 425 words; want 3..=25
2026-09-06T13:09:01.8025195Z   [must] requirement_quality REQ-SEAL-ENROLL-SHORTCUT-E criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8025508Z   [must] requirement_quality REQ-SEAL-ENROLL-SHORTCUT-E criterion=length — title is 232 words; want 3..=25
2026-09-06T13:09:01.8025888Z   [must] requirement_quality REQ-SEAL-ENROLL-VERB criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8026184Z   [must] requirement_quality REQ-SEAL-ENROLL-VERB criterion=length — title is 535 words; want 3..=25
2026-09-06T13:09:01.8026555Z   [must] requirement_quality REQ-SEAL-ENVELOPE-ATTR criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8026860Z   [must] requirement_quality REQ-SEAL-ENVELOPE-ATTR criterion=length — title is 300 words; want 3..=25
2026-09-06T13:09:01.8027270Z   [must] requirement_quality REQ-SEAL-FIDO2-RC-CLIENT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8027571Z   [must] requirement_quality REQ-SEAL-FIDO2-RC-CLIENT criterion=length — title is 284 words; want 3..=25
2026-09-06T13:09:01.8028004Z   [must] requirement_quality REQ-SEAL-FIDO2-SIG-VERIFY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8028301Z   [must] requirement_quality REQ-SEAL-FIDO2-SIG-VERIFY criterion=length — title is 406 words; want 3..=25
2026-09-06T13:09:01.8028679Z   [must] requirement_quality REQ-SEAL-MINT-VERB criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8029051Z   [must] requirement_quality REQ-SEAL-MINT-VERB criterion=length — title is 295 words; want 3..=25
2026-09-06T13:09:01.8029460Z   [must] requirement_quality REQ-SEAL-NO-CEREMONY-SURFACE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8029780Z   [must] requirement_quality REQ-SEAL-NO-CEREMONY-SURFACE criterion=length — title is 199 words; want 3..=25
2026-09-06T13:09:01.8030272Z   [must] requirement_quality REQ-SEAL-RECORD criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8030544Z   [must] requirement_quality REQ-SEAL-RECORD criterion=length — title is 281 words; want 3..=25
2026-09-06T13:09:01.8030929Z   [must] requirement_quality REQ-SEAL-SEND-SEALED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8031267Z   [must] requirement_quality REQ-SEAL-SEND-SEALED criterion=length — title is 485 words; want 3..=25
2026-09-06T13:09:01.8034953Z   [must] requirement_quality REQ-SEAL-STORE-REPLICATES-SUBNET-SCOPED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8035379Z   [must] requirement_quality REQ-SEAL-STORE-REPLICATES-SUBNET-SCOPED criterion=length — title is 439 words; want 3..=25
2026-09-06T13:09:01.8035825Z   [must] requirement_quality REQ-SEAL-SUBNET-BINDING-DEFAULT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8036176Z   [must] requirement_quality REQ-SEAL-SUBNET-BINDING-DEFAULT criterion=length — title is 255 words; want 3..=25
2026-09-06T13:09:01.8036571Z   [must] requirement_quality REQ-SEAL-TOKEN-FORMAT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8036862Z   [must] requirement_quality REQ-SEAL-TOKEN-FORMAT criterion=length — title is 239 words; want 3..=25
2026-09-06T13:09:01.8037377Z   [must] requirement_quality REQ-SEAL-VERIFY-CONTENT-BOUND criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8037701Z   [must] requirement_quality REQ-SEAL-VERIFY-CONTENT-BOUND criterion=length — title is 505 words; want 3..=25
2026-09-06T13:09:01.8037970Z   [must] requirement_quality REQ-SEAM-SPAWN criterion=length — title is 2 word(s); want 3..=25
2026-09-06T13:09:01.8038368Z   [must] requirement_quality REQ-SEAT-LIFETIME-BOUNDED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8038681Z   [must] requirement_quality REQ-SEAT-LIFETIME-BOUNDED criterion=length — title is 144 words; want 3..=25
2026-09-06T13:09:01.8039225Z   [must] requirement_quality REQ-SEC-REPLY-EXEMPTION-SEND-LEG criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8039568Z   [must] requirement_quality REQ-SEC-REPLY-EXEMPTION-SEND-LEG criterion=length — title is 650 words; want 3..=25
2026-09-06T13:09:01.8039992Z   [must] requirement_quality REQ-SELF-ADDR-REBIND-READVERTISE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8040326Z   [must] requirement_quality REQ-SELF-ADDR-REBIND-READVERTISE criterion=length — title is 199 words; want 3..=25
2026-09-06T13:09:01.8040736Z   [must] requirement_quality REQ-SELF-DETECT-PARENT-PID criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8041046Z   [must] requirement_quality REQ-SELF-DETECT-PARENT-PID criterion=length — title is 224 words; want 3..=25
2026-09-06T13:09:01.8041455Z   [must] requirement_quality REQ-SELF-ENDPOINT-ADDR-SURFACE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8041775Z   [must] requirement_quality REQ-SELF-ENDPOINT-ADDR-SURFACE criterion=length — title is 226 words; want 3..=25
2026-09-06T13:09:01.8042218Z   [must] requirement_quality REQ-SELF-ID-TRUST-INJECTED-ENV criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8042532Z   [must] requirement_quality REQ-SELF-ID-TRUST-INJECTED-ENV criterion=length — title is 232 words; want 3..=25
2026-09-06T13:09:01.8042933Z   [must] requirement_quality REQ-SEND-REPLYTO-REMOVE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8043243Z   [must] requirement_quality REQ-SEND-REPLYTO-REMOVE criterion=length — title is 60 words; want 3..=25
2026-09-06T13:09:01.8043793Z   [must] requirement_quality REQ-SEND-SPT-HOSTED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8044079Z   [must] requirement_quality REQ-SEND-SPT-HOSTED criterion=length — title is 169 words; want 3..=25
2026-09-06T13:09:01.8044460Z   [must] requirement_quality REQ-SEND-STAMP-AGENT-ID criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8044803Z   [must] requirement_quality REQ-SEND-STAMP-AGENT-ID criterion=length — title is 189 words; want 3..=25
2026-09-06T13:09:01.8045242Z   [must] requirement_quality REQ-SEND-WINDOW-DRAIN-HONOR criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8045557Z   [must] requirement_quality REQ-SEND-WINDOW-DRAIN-HONOR criterion=length — title is 359 words; want 3..=25
2026-09-06T13:09:01.8045877Z   [must] requirement_quality REQ-SERVE-OWNERSHIP-GENERATION criterion=length — title is 131 words; want 3..=25
2026-09-06T13:09:01.8046306Z   [must] requirement_quality REQ-SESSION-ADAPTER-RECORDED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8046614Z   [must] requirement_quality REQ-SESSION-ADAPTER-RECORDED criterion=length — title is 205 words; want 3..=25
2026-09-06T13:09:01.8047022Z   [must] requirement_quality REQ-SESSION-RESUME-TEMPLATE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8047388Z   [must] requirement_quality REQ-SESSION-RESUME-TEMPLATE criterion=length — title is 287 words; want 3..=25
2026-09-06T13:09:01.8047833Z   [must] requirement_quality REQ-SESSIONS-LOG-ENDPOINT-ATTRIBUTION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8048187Z   [must] requirement_quality REQ-SESSIONS-LOG-ENDPOINT-ATTRIBUTION criterion=length — title is 120 words; want 3..=25
2026-09-06T13:09:01.8048444Z   [must] requirement_quality REQ-SHELL-1 criterion=length — title is 36 words; want 3..=25
2026-09-06T13:09:01.8048816Z   [must] requirement_quality REQ-SHELL-2 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8049152Z   [must] requirement_quality REQ-SHELL-2 criterion=length — title is 104 words; want 3..=25
2026-09-06T13:09:01.8049413Z   [must] requirement_quality REQ-SHELL-3 criterion=length — title is 80 words; want 3..=25
2026-09-06T13:09:01.8049761Z   [must] requirement_quality REQ-SHELL-4 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8049998Z   [must] requirement_quality REQ-SHELL-4 criterion=length — title is 84 words; want 3..=25
2026-09-06T13:09:01.8050242Z   [must] requirement_quality REQ-SHELL-5 criterion=length — title is 49 words; want 3..=25
2026-09-06T13:09:01.8050700Z   [must] requirement_quality REQ-SHELL-ADAPTER-OWNED-DETACHED-SERVICE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8051082Z   [must] requirement_quality REQ-SHELL-ADAPTER-OWNED-DETACHED-SERVICE criterion=length — title is 1004 words; want 3..=25
2026-09-06T13:09:01.8051505Z   [must] requirement_quality REQ-SHELL-CLI-SPAWN-JOB-EXPOSURE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8051829Z   [must] requirement_quality REQ-SHELL-CLI-SPAWN-JOB-EXPOSURE criterion=length — title is 759 words; want 3..=25
2026-09-06T13:09:01.8052129Z   [must] requirement_quality REQ-SHELL-FRAME-VOCAB criterion=length — title is 253 words; want 3..=25
2026-09-06T13:09:01.8052496Z   [must] requirement_quality REQ-SHELL-HINTS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8052764Z   [must] requirement_quality REQ-SHELL-HINTS criterion=length — title is 479 words; want 3..=25
2026-09-06T13:09:01.8053178Z   [must] requirement_quality REQ-SHELL-KILL-AUTHENTICATED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8053604Z   [must] requirement_quality REQ-SHELL-KILL-AUTHENTICATED criterion=length — title is 878 words; want 3..=25
2026-09-06T13:09:01.8054041Z   [must] requirement_quality REQ-SHELL-LIST-DERIVED-PROVENANCE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8054377Z   [must] requirement_quality REQ-SHELL-LIST-DERIVED-PROVENANCE criterion=length — title is 167 words; want 3..=25
2026-09-06T13:09:01.8054892Z   [must] requirement_quality REQ-SHELL-OWNER-ONLINE-RESTORE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8055226Z   [must] requirement_quality REQ-SHELL-OWNER-ONLINE-RESTORE criterion=length — title is 520 words; want 3..=25
2026-09-06T13:09:01.8055617Z   [must] requirement_quality REQ-SHELL-PERCH-DIR criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8055893Z   [must] requirement_quality REQ-SHELL-PERCH-DIR criterion=length — title is 275 words; want 3..=25
2026-09-06T13:09:01.8056360Z   [must] requirement_quality REQ-SHELL-PERSISTENT-BOOT-RESTORE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8056684Z   [must] requirement_quality REQ-SHELL-PERSISTENT-BOOT-RESTORE criterion=length — title is 655 words; want 3..=25
2026-09-06T13:09:01.8057122Z   [must] requirement_quality REQ-SHELL-RELINK-FORCE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8057473Z   [must] requirement_quality REQ-SHELL-RELINK-FORCE criterion=length — title is 831 words; want 3..=25
2026-09-06T13:09:01.8057916Z   [must] requirement_quality REQ-SOFT-END-PRESERVES-LIVE-LISTENER criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8058269Z   [must] requirement_quality REQ-SOFT-END-PRESERVES-LIVE-LISTENER criterion=length — title is 246 words; want 3..=25
2026-09-06T13:09:01.8058709Z   [must] requirement_quality REQ-SPAWN-COLLISION-GUARD-LIVE-DUP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8059118Z   [must] requirement_quality REQ-SPAWN-COLLISION-GUARD-LIVE-DUP criterion=length — title is 480 words; want 3..=25
2026-09-06T13:09:01.8059424Z   [must] requirement_quality REQ-SPAWN-FRESH-TRUTHFUL criterion=length — title is 162 words; want 3..=25
2026-09-06T13:09:01.8059736Z   [must] requirement_quality REQ-SPOOL-TAKE-AUDIT criterion=length — title is 50 words; want 3..=25
2026-09-06T13:09:01.8060163Z   [must] requirement_quality REQ-STAMP-CONVERGENCE-ORDER criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8060487Z   [must] requirement_quality REQ-STAMP-CONVERGENCE-ORDER criterion=length — title is 108 words; want 3..=25
2026-09-06T13:09:01.8060844Z   [must] requirement_quality REQ-START-5 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8061108Z   [must] requirement_quality REQ-START-5 criterion=length — title is 129 words; want 3..=25
2026-09-06T13:09:01.8061365Z   [must] requirement_quality REQ-STORE-1 criterion=length — title is 34 words; want 3..=25
2026-09-06T13:09:01.8061684Z   [must] requirement_quality REQ-STORE-CONTEXT-BRANCH-FILL criterion=length — title is 73 words; want 3..=25
2026-09-06T13:09:01.8062060Z   [must] requirement_quality REQ-STREAM-INFOS-SERVER-FILTER criterion=length — title is 81 words; want 3..=25
2026-09-06T13:09:01.8062365Z   [must] requirement_quality REQ-STREAM-LEASE-CLASSES criterion=length — title is 168 words; want 3..=25
2026-09-06T13:09:01.8062770Z   [must] requirement_quality REQ-STREAM-LIFETIME-CLASS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8063080Z   [must] requirement_quality REQ-STREAM-LIFETIME-CLASS criterion=length — title is 764 words; want 3..=25
2026-09-06T13:09:01.8063481Z   [must] requirement_quality REQ-STREAM-OPENER-DURABLE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8063977Z   [must] requirement_quality REQ-STREAM-OPENER-DURABLE criterion=length — title is 155 words; want 3..=25
2026-09-06T13:09:01.8064425Z   [must] requirement_quality REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8064776Z   [must] requirement_quality REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE criterion=length — title is 192 words; want 3..=25
2026-09-06T13:09:01.8065031Z   [must] requirement_quality REQ-SUBNET-5 criterion=length — title is 52 words; want 3..=25
2026-09-06T13:09:01.8065395Z   [must] requirement_quality REQ-SUBNET-6 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8065642Z   [must] requirement_quality REQ-SUBNET-6 criterion=length — title is 38 words; want 3..=25
2026-09-06T13:09:01.8065994Z   [must] requirement_quality REQ-SUBNET-7 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8066253Z   [must] requirement_quality REQ-SUBNET-7 criterion=length — title is 75 words; want 3..=25
2026-09-06T13:09:01.8066501Z   [must] requirement_quality REQ-SUBNET-8 criterion=length — title is 53 words; want 3..=25
2026-09-06T13:09:01.8066906Z   [must] requirement_quality REQ-SUBNET-ADMIN-CODE-JOIN criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8067273Z   [must] requirement_quality REQ-SUBNET-ADMIN-CODE-JOIN criterion=length — title is 155 words; want 3..=25
2026-09-06T13:09:01.8067682Z   [must] requirement_quality REQ-SUBNET-ADMIN-RESURFACE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8067990Z   [must] requirement_quality REQ-SUBNET-ADMIN-RESURFACE criterion=length — title is 274 words; want 3..=25
2026-09-06T13:09:01.8068434Z   [must] requirement_quality REQ-SUBNET-ADMIN-SEED-REPLICATION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8068800Z   [must] requirement_quality REQ-SUBNET-ADMIN-SEED-REPLICATION criterion=length — title is 196 words; want 3..=25
2026-09-06T13:09:01.8069302Z   [must] requirement_quality REQ-SUBNET-ADMIN-SEED-ROTATION criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8069636Z   [must] requirement_quality REQ-SUBNET-ADMIN-SEED-ROTATION criterion=length — title is 209 words; want 3..=25
2026-09-06T13:09:01.8069936Z   [must] requirement_quality REQ-SUBNET-COUNT-ROUTABLE criterion=length — title is 78 words; want 3..=25
2026-09-06T13:09:01.8070362Z   [must] requirement_quality REQ-SUBNET-CREATE-CAPTURE-PROOF criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8070695Z   [must] requirement_quality REQ-SUBNET-CREATE-CAPTURE-PROOF criterion=length — title is 211 words; want 3..=25
2026-09-06T13:09:01.8071099Z   [must] requirement_quality REQ-SUBNET-DISPLAY-PARITY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8071404Z   [must] requirement_quality REQ-SUBNET-DISPLAY-PARITY criterion=length — title is 166 words; want 3..=25
2026-09-06T13:09:01.8071806Z   [must] requirement_quality REQ-SUBNET-DUAL-SEED-MINT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8072179Z   [must] requirement_quality REQ-SUBNET-DUAL-SEED-MINT criterion=length — title is 198 words; want 3..=25
2026-09-06T13:09:01.8072574Z   [must] requirement_quality REQ-SUBNET-EMPOWER-VERB criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8072870Z   [must] requirement_quality REQ-SUBNET-EMPOWER-VERB criterion=length — title is 221 words; want 3..=25
2026-09-06T13:09:01.8073292Z   [must] requirement_quality REQ-SUBNET-KEY-SCREENS-LABELLED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8073717Z   [must] requirement_quality REQ-SUBNET-KEY-SCREENS-LABELLED criterion=length — title is 509 words; want 3..=25
2026-09-06T13:09:01.8074128Z   [must] requirement_quality REQ-SUBNET-MINT-CAPTURES-MODE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8074443Z   [must] requirement_quality REQ-SUBNET-MINT-CAPTURES-MODE criterion=length — title is 483 words; want 3..=25
2026-09-06T13:09:01.8074856Z   [must] requirement_quality REQ-SUBNET-REVOKE-ADMIN-GATE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8075185Z   [must] requirement_quality REQ-SUBNET-REVOKE-ADMIN-GATE criterion=length — title is 164 words; want 3..=25
2026-09-06T13:09:01.8075587Z   [must] requirement_quality REQ-SUBNET-STATUS-MODES criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8075873Z   [must] requirement_quality REQ-SUBNET-STATUS-MODES criterion=length — title is 160 words; want 3..=25
2026-09-06T13:09:01.8076332Z   [must] requirement_quality REQ-SUBNET-STATUS-MODES-EVERY-VIEW criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8076666Z   [must] requirement_quality REQ-SUBNET-STATUS-MODES-EVERY-VIEW criterion=length — title is 273 words; want 3..=25
2026-09-06T13:09:01.8077094Z   [must] requirement_quality REQ-TEARDOWN-UNCOOPERATIVE-HOST criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8077475Z   [must] requirement_quality REQ-TEARDOWN-UNCOOPERATIVE-HOST criterion=length — title is 136 words; want 3..=25
2026-09-06T13:09:01.8077824Z   [must] requirement_quality REQ-TERM-5 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8078076Z   [must] requirement_quality REQ-TERM-5 criterion=length — title is 71 words; want 3..=25
2026-09-06T13:09:01.8078421Z   [must] requirement_quality REQ-TERM-6 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8078674Z   [must] requirement_quality REQ-TERM-6 criterion=length — title is 53 words; want 3..=25
2026-09-06T13:09:01.8079093Z   [must] requirement_quality REQ-TERM-7 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8079370Z   [must] requirement_quality REQ-TERM-7 criterion=length — title is 216 words; want 3..=25
2026-09-06T13:09:01.8079780Z   [must] requirement_quality REQ-TERM-ECHO-CLAMP-WINDOW criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8080085Z   [must] requirement_quality REQ-TERM-ECHO-CLAMP-WINDOW criterion=length — title is 291 words; want 3..=25
2026-09-06T13:09:01.8080544Z   [must] requirement_quality REQ-TEST-DAEMON-EPHEMERAL-ADVISORY-PORTS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8080912Z   [must] requirement_quality REQ-TEST-DAEMON-EPHEMERAL-ADVISORY-PORTS criterion=length — title is 133 words; want 3..=25
2026-09-06T13:09:01.8081302Z   [must] requirement_quality REQ-TEST-ENV-ONE-LOCK criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8081594Z   [must] requirement_quality REQ-TEST-ENV-ONE-LOCK criterion=length — title is 539 words; want 3..=25
2026-09-06T13:09:01.8082104Z   [must] requirement_quality REQ-TEST-LIVENESS-ORACLE-AUTHENTICATED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8082462Z   [must] requirement_quality REQ-TEST-LIVENESS-ORACLE-AUTHENTICATED criterion=length — title is 410 words; want 3..=25
2026-09-06T13:09:01.8082905Z   [must] requirement_quality REQ-TEST-RIG-DAEMON-TEARDOWN-PROVEN criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8083239Z   [must] requirement_quality REQ-TEST-RIG-DAEMON-TEARDOWN-PROVEN criterion=length — title is 515 words; want 3..=25
2026-09-06T13:09:01.8083730Z   [must] requirement_quality REQ-TEST-TMPDIR-HYGIENE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8084020Z   [must] requirement_quality REQ-TEST-TMPDIR-HYGIENE criterion=length — title is 348 words; want 3..=25
2026-09-06T13:09:01.8084459Z   [must] requirement_quality REQ-TRANSLATE-BINARY-LIVENESS-DECAY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8084989Z   [must] requirement_quality REQ-TRANSLATE-BINARY-LIVENESS-DECAY criterion=length — title is 94 words; want 3..=25
2026-09-06T13:09:01.8085374Z   [must] requirement_quality REQ-TRANSLATE-COMMAND criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8085665Z   [must] requirement_quality REQ-TRANSLATE-COMMAND criterion=length — title is 137 words; want 3..=25
2026-09-06T13:09:01.8086100Z   [must] requirement_quality REQ-TRANSLATE-COMMIT-MISS-TOLERANCE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8086438Z   [must] requirement_quality REQ-TRANSLATE-COMMIT-MISS-TOLERANCE criterion=length — title is 131 words; want 3..=25
2026-09-06T13:09:01.8086819Z   [must] requirement_quality REQ-TRUST-WARNING criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8087096Z   [must] requirement_quality REQ-TRUST-WARNING criterion=length — title is 898 words; want 3..=25
2026-09-06T13:09:01.8087498Z   [must] requirement_quality REQ-TRUST-WARNING-CADENCE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8087798Z   [must] requirement_quality REQ-TRUST-WARNING-CADENCE criterion=length — title is 961 words; want 3..=25
2026-09-06T13:09:01.8088204Z   [must] requirement_quality REQ-TRUST-WARNING-ENVELOPE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8088528Z   [must] requirement_quality REQ-TRUST-WARNING-ENVELOPE criterion=length — title is 724 words; want 3..=25
2026-09-06T13:09:01.8089052Z   [must] requirement_quality REQ-TRUST-WARNING-OVERRIDE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8089381Z   [must] requirement_quality REQ-TRUST-WARNING-OVERRIDE criterion=length — title is 624 words; want 3..=25
2026-09-06T13:09:01.8089825Z   [must] requirement_quality REQ-UNLISTED-ANSWERED-CROSS-NODE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8090159Z   [must] requirement_quality REQ-UNLISTED-ANSWERED-CROSS-NODE criterion=length — title is 518 words; want 3..=25
2026-09-06T13:09:01.8090569Z   [must] requirement_quality REQ-UNLISTED-CONTACT-LEDGER criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8090883Z   [must] requirement_quality REQ-UNLISTED-CONTACT-LEDGER criterion=length — title is 234 words; want 3..=25
2026-09-06T13:09:01.8091273Z   [must] requirement_quality REQ-UNLISTED-EVIDENCE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8091567Z   [must] requirement_quality REQ-UNLISTED-EVIDENCE criterion=length — title is 348 words; want 3..=25
2026-09-06T13:09:01.8091967Z   [must] requirement_quality REQ-UNLISTED-EVIDENCE-ROUTE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8092335Z   [must] requirement_quality REQ-UNLISTED-EVIDENCE-ROUTE criterion=length — title is 237 words; want 3..=25
2026-09-06T13:09:01.8092740Z   [must] requirement_quality REQ-UNLISTED-FOOTER-OMITTED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8093050Z   [must] requirement_quality REQ-UNLISTED-FOOTER-OMITTED criterion=length — title is 368 words; want 3..=25
2026-09-06T13:09:01.8093451Z   [must] requirement_quality REQ-UNLISTED-PRESENCE-PROBE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8093852Z   [must] requirement_quality REQ-UNLISTED-PRESENCE-PROBE criterion=length — title is 345 words; want 3..=25
2026-09-06T13:09:01.8094232Z   [must] requirement_quality REQ-UNLISTED-RENDER criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8094522Z   [must] requirement_quality REQ-UNLISTED-RENDER criterion=length — title is 191 words; want 3..=25
2026-09-06T13:09:01.8094919Z   [must] requirement_quality REQ-UPD-6 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8095166Z   [must] requirement_quality REQ-UPD-6 criterion=length — title is 32 words; want 3..=25
2026-09-06T13:09:01.8095506Z   [must] requirement_quality REQ-UPD-7 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8095747Z   [must] requirement_quality REQ-UPD-7 criterion=length — title is 88 words; want 3..=25
2026-09-06T13:09:01.8096096Z   [must] requirement_quality REQ-UPD-8 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8096338Z   [must] requirement_quality REQ-UPD-8 criterion=length — title is 115 words; want 3..=25
2026-09-06T13:09:01.8096585Z   [must] requirement_quality REQ-UPD-9 criterion=length — title is 110 words; want 3..=25
2026-09-06T13:09:01.8096909Z   [must] requirement_quality REQ-UPDATE-ADAPTERS-VERB criterion=length — title is 113 words; want 3..=25
2026-09-06T13:09:01.8097379Z   [must] requirement_quality REQ-UPDATE-APPLY-ALREADY-APPLIED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8097709Z   [must] requirement_quality REQ-UPDATE-APPLY-ALREADY-APPLIED criterion=length — title is 120 words; want 3..=25
2026-09-06T13:09:01.8098130Z   [must] requirement_quality REQ-UPDATE-APPLY-RESTART-NOTICE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8098463Z   [must] requirement_quality REQ-UPDATE-APPLY-RESTART-NOTICE criterion=length — title is 81 words; want 3..=25
2026-09-06T13:09:01.8098906Z   [must] requirement_quality REQ-UPDATE-COMPOSITE-EXIT-PRECEDENCE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8099339Z   [must] requirement_quality REQ-UPDATE-COMPOSITE-EXIT-PRECEDENCE criterion=length — title is 479 words; want 3..=25
2026-09-06T13:09:01.8099757Z   [must] requirement_quality REQ-UPDATE-DEFAULT-COMPOSITE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8100095Z   [must] requirement_quality REQ-UPDATE-DEFAULT-COMPOSITE criterion=length — title is 135 words; want 3..=25
2026-09-06T13:09:01.8100457Z   [must] requirement_quality REQ-UPDATE-FETCH-APPLY-FLAG criterion=length — title is 123 words; want 3..=25
2026-09-06T13:09:01.8100867Z   [must] requirement_quality REQ-UPDATE-FETCH-CURRENT-UX criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8101182Z   [must] requirement_quality REQ-UPDATE-FETCH-CURRENT-UX criterion=length — title is 134 words; want 3..=25
2026-09-06T13:09:01.8101598Z   [must] requirement_quality REQ-UPDATE-FINISH-COMMUNE-FLUSH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8101990Z   [must] requirement_quality REQ-UPDATE-FINISH-COMMUNE-FLUSH criterion=length — title is 215 words; want 3..=25
2026-09-06T13:09:01.8102333Z   [must] requirement_quality REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL criterion=length — title is 74 words; want 3..=25
2026-09-06T13:09:01.8102723Z   [must] requirement_quality REQ-UPDATE-GH-TRANSPORT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8103019Z   [must] requirement_quality REQ-UPDATE-GH-TRANSPORT criterion=length — title is 169 words; want 3..=25
2026-09-06T13:09:01.8103424Z   [must] requirement_quality REQ-UPDATE-ONE-SHOT-FINISH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8103831Z   [must] requirement_quality REQ-UPDATE-ONE-SHOT-FINISH criterion=length — title is 94 words; want 3..=25
2026-09-06T13:09:01.8104235Z   [must] requirement_quality REQ-UPDATE-PROMOTE-DRAINED criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8104615Z   [must] requirement_quality REQ-UPDATE-PROMOTE-DRAINED criterion=length — title is 167 words; want 3..=25
2026-09-06T13:09:01.8105044Z   [must] requirement_quality REQ-UPDATE-REFUSAL-EXIT-DISTINCT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8105373Z   [must] requirement_quality REQ-UPDATE-REFUSAL-EXIT-DISTINCT criterion=length — title is 490 words; want 3..=25
2026-09-06T13:09:01.8105784Z   [must] requirement_quality REQ-UPDATE-RESTART-SAFE-SWAP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8106109Z   [must] requirement_quality REQ-UPDATE-RESTART-SAFE-SWAP criterion=length — title is 158 words; want 3..=25
2026-09-06T13:09:01.8106527Z   [must] requirement_quality REQ-UPDATE-RUNNING-IMAGE-SURFACE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8106856Z   [must] requirement_quality REQ-UPDATE-RUNNING-IMAGE-SURFACE criterion=length — title is 337 words; want 3..=25
2026-09-06T13:09:01.8107272Z   [must] requirement_quality REQ-UPDATE-TRIAL-DRAIN-DRIVE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8107582Z   [must] requirement_quality REQ-UPDATE-TRIAL-DRAIN-DRIVE criterion=length — title is 464 words; want 3..=25
2026-09-06T13:09:01.8107989Z   [must] requirement_quality REQ-USHER-LIFECYCLE-VERBS criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8108289Z   [must] requirement_quality REQ-USHER-LIFECYCLE-VERBS criterion=length — title is 647 words; want 3..=25
2026-09-06T13:09:01.8108727Z   [must] requirement_quality REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8109137Z   [must] requirement_quality REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT criterion=length — title is 227 words; want 3..=25
2026-09-06T13:09:01.8109548Z   [must] requirement_quality REQ-VIS-REMOTE-NOT-HIDE-NEW criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8109865Z   [must] requirement_quality REQ-VIS-REMOTE-NOT-HIDE-NEW criterion=length — title is 326 words; want 3..=25
2026-09-06T13:09:01.8110254Z   [must] requirement_quality REQ-VOCAB-ANCHOR-SUBNET criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8110545Z   [must] requirement_quality REQ-VOCAB-ANCHOR-SUBNET criterion=length — title is 605 words; want 3..=25
2026-09-06T13:09:01.8110831Z   [must] requirement_quality REQ-WAKE-RESUME-LEG criterion=length — title is 274 words; want 3..=25
2026-09-06T13:09:01.8111098Z   [must] requirement_quality REQ-WAKE-WAIT criterion=length — title is 89 words; want 3..=25
2026-09-06T13:09:01.8111519Z   [must] requirement_quality REQ-WAN-DENY-PRECEDES-EXISTENCE criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8111905Z   [must] requirement_quality REQ-WAN-DENY-PRECEDES-EXISTENCE criterion=length — title is 301 words; want 3..=25
2026-09-06T13:09:01.8112294Z   [must] requirement_quality REQ-WAN-SEND-DELIVERY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8112579Z   [must] requirement_quality REQ-WAN-SEND-DELIVERY criterion=length — title is 117 words; want 3..=25
2026-09-06T13:09:01.8112884Z   [must] requirement_quality REQ-WAN-SPT-HOSTED-DELIVERY criterion=length — title is 156 words; want 3..=25
2026-09-06T13:09:01.8113241Z   [must] requirement_quality REQ-WHOAMI-1 criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8113598Z   [must] requirement_quality REQ-WHOAMI-1 criterion=length — title is 101 words; want 3..=25
2026-09-06T13:09:01.8114022Z   [must] requirement_quality REQ-WHOAMI-EXPLICIT-SID-REFUSAL criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8114347Z   [must] requirement_quality REQ-WHOAMI-EXPLICIT-SID-REFUSAL criterion=length — title is 602 words; want 3..=25
2026-09-06T13:09:01.8114795Z   [must] requirement_quality REQ-WHOAMI-IDENTITY-ONLY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8115091Z   [must] requirement_quality REQ-WHOAMI-IDENTITY-ONLY criterion=length — title is 125 words; want 3..=25
2026-09-06T13:09:01.8115495Z   [must] requirement_quality REQ-WORKER-LIST-VISIBILITY criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8115815Z   [must] requirement_quality REQ-WORKER-LIST-VISIBILITY criterion=length — title is 82 words; want 3..=25
2026-09-06T13:09:01.8116200Z   [must] requirement_quality REQ-WORKER-MINTED-NAME criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8116482Z   [must] requirement_quality REQ-WORKER-MINTED-NAME criterion=length — title is 67 words; want 3..=25
2026-09-06T13:09:01.8116792Z   [must] requirement_quality REQ-WORKER-PICKER-EXCLUDED criterion=length — title is 58 words; want 3..=25
2026-09-06T13:09:01.8117154Z   [must] requirement_quality REQ-WORKER-REAP criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8117421Z   [must] requirement_quality REQ-WORKER-REAP criterion=length — title is 116 words; want 3..=25
2026-09-06T13:09:01.8117837Z   [must] requirement_quality REQ-WORKER-SID-SYMMETRIC-AUTH criterion=contains-and — title contains ' and ' — may smuggle multiple capabilities
2026-09-06T13:09:01.8118147Z   [must] requirement_quality REQ-WORKER-SID-SYMMETRIC-AUTH criterion=length — title is 125 words; want 3..=25
2026-09-06T13:09:01.8118471Z   [must] requirement_quality REQ-XTASK-SPT-BIN-TARGET-DIR criterion=length — title is 98 words; want 3..=25
2026-09-06T13:09:01.8118500Z 
2026-09-06T13:09:01.8118601Z # Requirement quality review
2026-09-06T13:09:01.8118639Z 
2026-09-06T13:09:01.8118834Z You are reviewing 876 requirement(s) from `traceable-reqs.toml` against a quality
2026-09-06T13:09:01.8119120Z rubric. Deterministic checks (length, contains-and, tbd-todo, duplicate-titles,
2026-09-06T13:09:01.8119306Z trailing-etc) have already run and surfaced as `requirement_quality` findings on
2026-09-06T13:09:01.8119449Z this command's output. Your task is the rubric items below.
2026-09-06T13:09:01.8119478Z 
2026-09-06T13:09:01.8119560Z ## Rubric
2026-09-06T13:09:01.8119593Z 
2026-09-06T13:09:01.8119846Z - **singular** — describes one capability; no smuggled "and"/"or" across distinct actions.
2026-09-06T13:09:01.8120079Z - **verifiable** — states an observable behavior a test or reviewer could confirm.
2026-09-06T13:09:01.8120288Z - **atomic** — cannot be split into two requirements without losing meaning.
2026-09-06T13:09:01.8120435Z - **active-voice** — clear subject and active verb.
2026-09-06T13:09:01.8120469Z 
2026-09-06T13:09:01.8120699Z If a criterion is borderline or doesn't apply, abstain — only emit findings for
2026-09-06T13:09:01.8120853Z clear concerns.
2026-09-06T13:09:01.8120881Z 
2026-09-06T13:09:01.8120967Z ## Requirements
2026-09-06T13:09:01.8120990Z 
2026-09-06T13:09:01.8121066Z ### REQ-ARCH-1
2026-09-06T13:09:01.8121178Z - Title: Many small acyclically-layered crates
2026-09-06T13:09:01.8121268Z - Required stages: impl
2026-09-06T13:09:01.8121300Z 
2026-09-06T13:09:01.8121406Z ### REQ-ARCH-2
2026-09-06T13:09:01.8121554Z - Title: Public SDK surface is spt-proto, spt-runtime, spt-msg
2026-09-06T13:09:01.8121662Z - Required stages: impl
2026-09-06T13:09:01.8121696Z 
2026-09-06T13:09:01.8121783Z ### REQ-ARCH-3
2026-09-06T13:09:01.8122065Z - Title: Wire-protocol version independent of crate semver, N-1 compat window
2026-09-06T13:09:01.8122164Z - Required stages: impl, unit
2026-09-06T13:09:01.8122197Z 
2026-09-06T13:09:01.8122278Z ### REQ-ARCH-4
2026-09-06T13:09:01.8122432Z - Title: Copy-verbatim the commodity layer from the sister project
2026-09-06T13:09:01.8122525Z - Required stages: impl, unit
2026-09-06T13:09:01.8122607Z 
2026-09-06T13:09:01.8122695Z ### REQ-DAEMON-1
2026-09-06T13:09:01.8122840Z - Title: One per-machine spt-daemon owning all per-machine state
2026-09-06T13:09:01.8122940Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8122968Z 
2026-09-06T13:09:01.8123054Z ### REQ-DAEMON-2
2026-09-06T13:09:01.8123176Z - Title: Broker/brain split for seamless self-update
2026-09-06T13:09:01.8123276Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8123304Z 
2026-09-06T13:09:01.8123391Z ### REQ-DAEMON-3
2026-09-06T13:09:01.8123524Z - Title: Any api invocation auto-starts the daemon if absent
2026-09-06T13:09:01.8123624Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8123653Z 
2026-09-06T13:09:01.8123734Z ### REQ-DAEMON-4
2026-09-06T13:09:01.8123843Z - Title: Honor every KNOWN-HAZARDS invariant
2026-09-06T13:09:01.8123943Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8123973Z 
2026-09-06T13:09:01.8124054Z ### REQ-STORE-1
2026-09-06T13:09:01.8124869Z - Title: spt-store::BranchStore (git branch as versioned KV; commit=checkpoint/tip=resume, atomic multi-key, merge-native sync) is the substrate for coarse/durable/audited state (context, registry snapshot+distribution, daemon checkpoint); hot paths (B5 fsync journal) + indexed queries (SQLite spool) excluded (ADR-0011)
2026-09-06T13:09:01.8124965Z - Required stages: impl, unit
2026-09-06T13:09:01.8124994Z 
2026-09-06T13:09:01.8125087Z ### REQ-MANIFEST-1
2026-09-06T13:09:01.8125251Z - Title: Per-adapter manifest with adapter_name and min_spt_core_version
2026-09-06T13:09:01.8125351Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8125397Z 
2026-09-06T13:09:01.8125503Z ### REQ-MANIFEST-2
2026-09-06T13:09:01.8125999Z - Title: Adapter profiles — sparse leaf-replace overlays (shipped + local), composite <adapter>:<profile> addressing, shadow-refusal, tighten-only consent floors
2026-09-06T13:09:01.8126107Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8126136Z 
2026-09-06T13:09:01.8126217Z ### REQ-MANIFEST-3
2026-09-06T13:09:01.8126813Z - Title: Adapter strings — [strings] KV tree, dot-path get-string resolving through the profile leaf-replace overlay, set-string editing a local profile's [strings] only; data-only (nothing executes a string)
2026-09-06T13:09:01.8126919Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8126947Z 
2026-09-06T13:09:01.8127033Z ### REQ-MANIFEST-4
2026-09-06T13:09:01.8127695Z - Title: Keyword hints — [[hints]] {keywords (literal/regex), text}; spt api hint --session emits at most one matched hint per message, once per session (seen-set), declaration-order first match; profiles overlay [[hints]] by leaf-replace
2026-09-06T13:09:01.8127806Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8127830Z 
2026-09-06T13:09:01.8127915Z ### REQ-MANIFEST-5
2026-09-06T13:09:01.8130928Z - Title: File-backed adapter [strings] (M12-W3-T3.1): a [strings] dot-path value MAY be an inline-table FILE POINTER `key = { file = "rel/path" }` resolved to the file's contents at get-string time, keeping large bodies (skill-instructions, hint text) out of the manifest. A value-position table with a `file` key IS the pointer form (reserved — cannot double as data). Per-adapter aux storage `adapters/<adapter>/strings/`; pointers resolve relative to it with CONTAINMENT (reject `..`/absolute escaping the dir). UPDATE-SAFETY: a LOCAL profile's file-pointers resolve relative to the user-owned local-profile dir (NOT adapter-shipped strings/, which adapter updates overwrite), or the local profile inlines. Validate-at-register (fail-fast on a bad/escaping/missing pointer) + LAZY read at get-string (live file edits reflect, no re-register) + skip-diagnostics on missing-at-read (no hard-crash, mirrors [digest]). Rides the same leaf-replace profile overlay as the rest of [strings].
2026-09-06T13:09:01.8131189Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8131217Z 
2026-09-06T13:09:01.8131298Z ### REQ-MANIFEST-6
2026-09-06T13:09:01.8133345Z - Title: Cross-adapter fallback target addressing (M12-W3-T3.2): a cross-adapter fallback target is addressed as `<adapter>:<profile>` (not just a bare adapter_name), resolved through the one composite-addressing resolver (registry::resolve_option) at every adapter-option read site so a fallback may select a shipped/local profile (e.g. a `ccs` profile). CONTEXT.md §cross-adapter-fallback reconciled ("ccs is a profile; cross-adapter fallback may target <adapter>:<profile>"). Contract-only this milestone: the node-wide fallback SETTING + its rate-limit invocation are deferred to the consuming milestone (the runtime path does not exist yet); this REQ guarantees the ADDRESSING resolves.
2026-09-06T13:09:01.8133500Z - Required stages: doc, unit
2026-09-06T13:09:01.8133528Z 
2026-09-06T13:09:01.8133614Z ### REQ-MANIFEST-7
2026-09-06T13:09:01.8136535Z - Title: Adapter-declared shortcut basename (M12-W2 follow-on): an optional `[adapter] shortcut_basename` manifest field names the basename the picker bakes into the generated `<basename>-<id>` launcher shortcut (REQ-RUN-SHORTCUT). Absent ⇒ the harness-agnostic default `spt` (→ `spt-<id>`); an adapter sets it to brand its shortcuts (claude-spt → `cc` → `cc-<id>`), so the Claude-Code-ness lives in the PUBLISHED adapter manifest, never hardcoded in spt-core. The picker reads it from the RESOLVED manifest of the selected adapter (registry::resolve_option), falling back to `spt` when absent/empty/unresolvable. Additive + N-1-safe (serde-default Option, omitted from serialization when absent; old manifests parse clean); manifest.schema.json regenerated from the derive (ADR-0001, CI drift-gated). Documented in docs/MANIFEST.md `[adapter]` section + the claude-spt worked example — the adapter-author contract perri builds spt-claude-code against.
2026-09-06T13:09:01.8136653Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8136677Z 
2026-09-06T13:09:01.8136763Z ### REQ-MANIFEST-8
2026-09-06T13:09:01.8138906Z - Title: [adapter] host_binaries declares the harness executable basenames a kind="harness" adapter hosts agents inside (e.g. host_binaries = ["claude"]); bind-time pid→exe-basename match (case-insensitive, .exe-stripped) over the seed's parent_pid selects the candidate adapter set; zero matches → a friendly error naming the binary + the --adapter escape hatch. Additive + N-1-safe: optional Vec<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] (omitted-serialized like shortcut_basename, old manifests parse clean); manifest.schema.json regenerated from the derive (ADR-0001, CI drift-gated). The match-key for ADR-0021 adapter-agnostic bind-time resolution. (v0.9.0)
2026-09-06T13:09:01.8139111Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8139149Z 
2026-09-06T13:09:01.8139234Z ### REQ-SEAM-SPAWN
2026-09-06T13:09:01.8139330Z - Title: spawn-session seam
2026-09-06T13:09:01.8139431Z - Required stages: impl, unit
2026-09-06T13:09:01.8139459Z 
2026-09-06T13:09:01.8139549Z ### REQ-SEAM-POSTSPAWN
2026-09-06T13:09:01.8139669Z - Title: post-spawn / api bind seam with boot nonce
2026-09-06T13:09:01.8139759Z - Required stages: impl, unit
2026-09-06T13:09:01.8139849Z 
2026-09-06T13:09:01.8139945Z ### REQ-SEAM-PSYCHE
2026-09-06T13:09:01.8140069Z - Title: spawn-psyche seam (fresh + resume templates)
2026-09-06T13:09:01.8140164Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8140192Z 
2026-09-06T13:09:01.8140279Z ### REQ-SEAM-HISTORY
2026-09-06T13:09:01.8140446Z - Title: History subsystem (fetcher / locate-normalize / native store)
2026-09-06T13:09:01.8140541Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8140569Z 
2026-09-06T13:09:01.8140657Z ### REQ-SEAM-ACTIVITY
2026-09-06T13:09:01.8140822Z - Title: Activity/idle reported via api sentinels, not PTY quiescence
2026-09-06T13:09:01.8141023Z - Required stages: impl, unit
2026-09-06T13:09:01.8141056Z 
2026-09-06T13:09:01.8141142Z ### REQ-SEAM-INJECT
2026-09-06T13:09:01.8141291Z - Title: inject-input methods configurable per activity-state
2026-09-06T13:09:01.8141380Z - Required stages: impl, unit
2026-09-06T13:09:01.8141408Z 
2026-09-06T13:09:01.8141498Z ### REQ-SEAM-RESUME
2026-09-06T13:09:01.8141711Z - Title: resume-session seam (fresh-with-preload / continue-existing)
2026-09-06T13:09:01.8141801Z - Required stages: 
2026-09-06T13:09:01.8141829Z 
2026-09-06T13:09:01.8141926Z ### REQ-SEAM-CAPABILITY
2026-09-06T13:09:01.8142054Z - Title: Hostable endpoint-types capability declaration
2026-09-06T13:09:01.8142148Z - Required stages: impl, unit
2026-09-06T13:09:01.8142178Z 
2026-09-06T13:09:01.8142269Z ### REQ-SEAM-UPDATE
2026-09-06T13:09:01.8142406Z - Title: Adapter-update avenue (file-pull / delegated command)
2026-09-06T13:09:01.8142497Z - Required stages: impl, unit
2026-09-06T13:09:01.8142531Z 
2026-09-06T13:09:01.8142612Z ### REQ-API-1
2026-09-06T13:09:01.8142764Z - Title: api prefix and adapter_name on every machinery invocation
2026-09-06T13:09:01.8142855Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8142885Z 
2026-09-06T13:09:01.8142969Z ### REQ-API-2
2026-09-06T13:09:01.8143151Z - Title: The api subcommand surface (bind/listen/poll/state/worker/boundary/...)
2026-09-06T13:09:01.8143246Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8143275Z 
2026-09-06T13:09:01.8143365Z ### REQ-API-3
2026-09-06T13:09:01.8143480Z - Title: commune/signoff are file-drops, not commands
2026-09-06T13:09:01.8143576Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8143604Z 
2026-09-06T13:09:01.8143684Z ### REQ-API-4
2026-09-06T13:09:01.8145136Z - Title: api resolves the adapter manifest (+ profile + install dir) from `--adapter name:profile` via the registry when `--manifest` is omitted; `--manifest` becomes an optional OVERRIDE (unregistered / local-dev manifests). Removes the require-both-flags redundancy — a registered adapter's live bringup / digest / capability needs only `--adapter` — and yields the precise install dir (the record's source_dir) rather than the --manifest parent, closing the copy-mode psyche-binary edge (v0.8.0)
2026-09-06T13:09:01.8145241Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8145270Z 
2026-09-06T13:09:01.8145354Z ### REQ-START-1
2026-09-06T13:09:01.8145527Z - Title: Adapters never resolve SPT_HOME; binary on PATH; api bridging only
2026-09-06T13:09:01.8145622Z - Required stages: impl, unit
2026-09-06T13:09:01.8145650Z 
2026-09-06T13:09:01.8145732Z ### REQ-START-2
2026-09-06T13:09:01.8145865Z - Title: Harness-hosted startup: api seed then listen
2026-09-06T13:09:01.8145955Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8145984Z 
2026-09-06T13:09:01.8146071Z ### REQ-START-3
2026-09-06T13:09:01.8146217Z - Title: spt-hosted startup: spawn-session then api bind (no file)
2026-09-06T13:09:01.8146318Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8146352Z 
2026-09-06T13:09:01.8146439Z ### REQ-START-4
2026-09-06T13:09:01.8146556Z - Title: Adapter-injected env aliases (SPT/OWL/LIVE)
2026-09-06T13:09:01.8146648Z - Required stages: impl, unit
2026-09-06T13:09:01.8146672Z 
2026-09-06T13:09:01.8146744Z ### REQ-START-5
2026-09-06T13:09:01.8149594Z - Title: Adapter-agnostic harness-hosted seed + bind-time adapter/profile resolution (ADR-0021): `api seed` carries only parent_pid + session_id (+ optional cwd), no --adapter — a pure "a harness session exists at this pid" record; --adapter becomes an OPTIONAL override across the whole api group (an explicit name[:profile] for adapter dev, never required). Omitted, listen/poll resolve the owning adapter/profile AT BIND as a pure read against the live registry — never a seed-time snapshot that can drift: seed parent_pid → exe basename → host_binaries candidate set (REQ-MANIFEST-8) → active-profile pointer (REQ-INSTALL-12) primary, else greatest-registered_at_ms candidate base profile (name-asc tie) → friendly zero-match error. Covers BOTH LiveAgent (listen) and ReadyAgent (poll) bringup. Restores legacy parity: `$LIVE start <id>` → `$SPT listen <id>` with no mandatory --adapter, one generic SessionStart hook per harness binary. (v0.9.0)
2026-09-06T13:09:01.8149856Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8149885Z 
2026-09-06T13:09:01.8149976Z ### REQ-EP-1
2026-09-06T13:09:01.8150099Z - Title: Day-one endpoint types; open type system
2026-09-06T13:09:01.8150239Z - Required stages: impl, unit
2026-09-06T13:09:01.8150268Z 
2026-09-06T13:09:01.8150348Z ### REQ-EP-2
2026-09-06T13:09:01.8150505Z - Title: Agent endpoints vs Shells distinction in the type model
2026-09-06T13:09:01.8150597Z - Required stages: impl, unit
2026-09-06T13:09:01.8150624Z 
2026-09-06T13:09:01.8150710Z ### REQ-EP-3
2026-09-06T13:09:01.8150878Z - Title: Messaging payloads carry typed operation commands + file blobs
2026-09-06T13:09:01.8150967Z - Required stages: impl, unit
2026-09-06T13:09:01.8150996Z 
2026-09-06T13:09:01.8151096Z ### REQ-EP-4
2026-09-06T13:09:01.8151222Z - Title: PresenceChannel broker endpoint (seam day-one)
2026-09-06T13:09:01.8151315Z - Required stages: impl, unit
2026-09-06T13:09:01.8151344Z 
2026-09-06T13:09:01.8151425Z ### REQ-EP-5
2026-09-06T13:09:01.8152049Z - Title: Concrete shell instantiation model: spawn-mints-instance (vs relink/online), registered-on-node permission + broadcast-is-discovery, per-shell require_approval gate, max_instances_per_owner + over_cap, instance aliasing, discovery scope
2026-09-06T13:09:01.8152149Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8152178Z 
2026-09-06T13:09:01.8152262Z ### REQ-EP-6
2026-09-06T13:09:01.8153711Z - Title: Gateway type acceptance: a Gateway-typed perch binds (api bind --type, open type system — un-hardcode the live_agent default), advertises/addressable like any endpoint, owns shells (owner validation not agent-family-gated), subscribes to digests, and is the user-msg identity gate's user-backed origin (REQ-MSG-5); in-tree mock-gateway fixture (R-DOCS-2 pattern, no downstream adapter code). Cross-node WAN Gateway-origin (registry endpoint_type trust) tracked by REQ-MSG-6
2026-09-06T13:09:01.8153826Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8153854Z 
2026-09-06T13:09:01.8153935Z ### REQ-EP-7
2026-09-06T13:09:01.8155566Z - Title: Durable live-role.md: a per-agent broad-purpose statement in tracked/agents/<id>/ beside live-context.md (replicates with the mind on the same a-<id> branch); renders FIRST at start-transition context injection (role -> live-context -> project-context); SOLE writer `spt endpoint role --overwrite <file>` — mechanical no-automated-writer guarantee (echo-commune ingest / signoff / Psyche reconcile structurally exclude it). The user-backed-origin hard gate on the writer is a deferred later tightening (rides the user-msg identity plumbing)
2026-09-06T13:09:01.8155673Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8155701Z 
2026-09-06T13:09:01.8155782Z ### REQ-EP-8
2026-09-06T13:09:01.8158637Z - Title: AlwaysOnEndpoint: a resident, addressable, mindless endpoint whose adapter binary the daemon supervises continuously — register-triggered by an adapter-option's `[always-on]` manifest section, one supervised binary per `<adapter>[:profile]`, running independent of agent liveness. It self-manages its `#`-addressed channel endpoints via the existing `api bind` (one connection fronts many). The SECOND class of spt-core-boot-launched third-party binary (after the shell wake-watcher); supervision reuses the wake-watcher scaffolding (backoff / give-up latch / one-per-instance lock / orphan-kill / brain-side reconcile) MINUS the offline-only flip — always online, never resting (no dormant/suspended states). Two-way: agents message it; it may call `endpoint wake <id>`, target-side authorized (REQ-INST-3/6 wake resolution + access whitelist + shell_wake_spawn_anywhere — no caller-ownership gate). First consumer downstream: spt-discord.
2026-09-06T13:09:01.8158789Z - Required stages: 
2026-09-06T13:09:01.8158818Z 
2026-09-06T13:09:01.8159061Z ### REQ-EP-9
2026-09-06T13:09:01.8160659Z - Title: `#` always-on address sigil: a reserved LEADING sigil marking an AlwaysOnEndpoint, extending the REQ-INST-10 grammar to `[subnet:]#id[@node]`. Mandatory + bijective — `#name` ⟺ always-on endpoint, bare `name` ⟺ agent endpoint — so the router resolves endpoint class from the address alone, before any registry lookup. Sits ABOVE REQ-HAZARD-ID-CHARSET: the address parser strips the single leading `#` before id validation, so the bare/stored id stays charset-clean and a mid-id `#` remains rejected (the charset contract is unchanged).
2026-09-06T13:09:01.8160813Z - Required stages: 
2026-09-06T13:09:01.8160842Z 
2026-09-06T13:09:01.8160923Z ### REQ-INST-1
2026-09-06T13:09:01.8161070Z - Title: endpoint ID vs instance split (adapter-agnostic ID)
2026-09-06T13:09:01.8161157Z - Required stages: 
2026-09-06T13:09:01.8161185Z 
2026-09-06T13:09:01.8161270Z ### REQ-INST-2
2026-09-06T13:09:01.8161384Z - Title: Per-node files, synced Psyche mind
2026-09-06T13:09:01.8161486Z - Required stages: impl, unit
2026-09-06T13:09:01.8161514Z 
2026-09-06T13:09:01.8161595Z ### REQ-INST-3
2026-09-06T13:09:01.8161729Z - Title: Dormant (warm) / suspended (cold) resting states
2026-09-06T13:09:01.8161833Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8161861Z 
2026-09-06T13:09:01.8161942Z ### REQ-INST-4
2026-09-06T13:09:01.8162101Z - Title: active to dormant/suspended fires a transition echo commune
2026-09-06T13:09:01.8162199Z - Required stages: impl, unit
2026-09-06T13:09:01.8162223Z 
2026-09-06T13:09:01.8162300Z ### REQ-INST-5
2026-09-06T13:09:01.8162463Z - Title: Two-tier context sync (live to all, project to same-project)
2026-09-06T13:09:01.8162552Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8162581Z 
2026-09-06T13:09:01.8162671Z ### REQ-INST-6
2026-09-06T13:09:01.8162836Z - Title: Deferred messages not delivered to dormant/suspended instances
2026-09-06T13:09:01.8162931Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8162959Z 
2026-09-06T13:09:01.8163055Z ### REQ-INST-7
2026-09-06T13:09:01.8163170Z - Title: Subnet registry + bare-id resolution policy
2026-09-06T13:09:01.8163269Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8163293Z 
2026-09-06T13:09:01.8163380Z ### REQ-INST-8
2026-09-06T13:09:01.8163517Z - Title: Remote-control mode distinct from local operation
2026-09-06T13:09:01.8163617Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8163651Z 
2026-09-06T13:09:01.8163732Z ### REQ-INST-9
2026-09-06T13:09:01.8163903Z - Title: Multi-subnet membership (same-user N subnets; cross-user seam)
2026-09-06T13:09:01.8163990Z - Required stages: impl, unit
2026-09-06T13:09:01.8164019Z 
2026-09-06T13:09:01.8164104Z ### REQ-INST-10
2026-09-06T13:09:01.8164286Z - Title: Qualified addressing [subnet:]id[@node] + ambiguity forces qualification
2026-09-06T13:09:01.8164376Z - Required stages: impl, unit
2026-09-06T13:09:01.8164404Z 
2026-09-06T13:09:01.8164490Z ### REQ-INST-11
2026-09-06T13:09:01.8164682Z - Title: spt rename <id> rippled to all instances (collision-checked, 6.5-reconciled)
2026-09-06T13:09:01.8164781Z - Required stages: impl, unit
2026-09-06T13:09:01.8164809Z 
2026-09-06T13:09:01.8164891Z ### REQ-INST-12
2026-09-06T13:09:01.8165152Z - Title: Endpoint visibility per-(endpoint,subnet): excluded semantics, OR-of-defaults + override, gates sync
2026-09-06T13:09:01.8165247Z - Required stages: impl, unit
2026-09-06T13:09:01.8165333Z 
2026-09-06T13:09:01.8165421Z ### REQ-INST-13
2026-09-06T13:09:01.8165579Z - Title: Subnet-exclusive sync + per-endpoint subnet-membership list
2026-09-06T13:09:01.8165675Z - Required stages: impl, unit
2026-09-06T13:09:01.8165699Z 
2026-09-06T13:09:01.8165784Z ### REQ-INST-14
2026-09-06T13:09:01.8166122Z - Title: Resource advertisement (subnet resource registry): free-text blurb, both-authored, registry projection, visibility/whitelist-gated
2026-09-06T13:09:01.8166223Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8166247Z 
2026-09-06T13:09:01.8166333Z ### REQ-INST-15
2026-09-06T13:09:01.8167081Z - Title: Immutable home subnet (assigned at creation: auto-if-one/ask-if-many) + spt fork (cross-subnet clone to a new identity, copy-then-diverge, not re-home); adapter chosen at creation from registered hostable adapters, changed only via launch/resume-under-new (ADR-0010)
2026-09-06T13:09:01.8167187Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8167215Z 
2026-09-06T13:09:01.8167300Z ### REQ-REACH-1
2026-09-06T13:09:01.8167463Z - Title: Off-node remote-drive detection + file transfer
2026-09-06T13:09:01.8167559Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8167582Z 
2026-09-06T13:09:01.8167667Z ### REQ-REACH-2
2026-09-06T13:09:01.8167807Z - Title: Remote command execution (deferred, consent-gated)
2026-09-06T13:09:01.8167892Z - Required stages: 
2026-09-06T13:09:01.8167920Z 
2026-09-06T13:09:01.8168005Z ### REQ-MSG-1
2026-09-06T13:09:01.8168410Z - Title: Local message delivery: TCP-first to a registered address, spool fallback when offline; id->address via registry (stale-clean first); reply routing (__REPLY_TO__)
2026-09-06T13:09:01.8168516Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8168545Z 
2026-09-06T13:09:01.8168625Z ### REQ-MSG-2
2026-09-06T13:09:01.8168868Z - Title: spt binary CLI surface: send/ring/ready(+--once)/list/stop/whoami, stable arg shapes + exit codes
2026-09-06T13:09:01.8169082Z - Required stages: impl, unit
2026-09-06T13:09:01.8169112Z 
2026-09-06T13:09:01.8169197Z ### REQ-MSG-3
2026-09-06T13:09:01.8169550Z - Title: Ready-agent lifecycle: register perch (info.json + listener + registry address) on ready, drain spooled backlog on startup, clean teardown
2026-09-06T13:09:01.8169645Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8169673Z 
2026-09-06T13:09:01.8169751Z ### REQ-MSG-4
2026-09-06T13:09:01.8170511Z - Title: Listener stream stdout emits EVENT envelope lines (sister-format, ADR-0001): parse the __REPLY_TO__ frame, pass pre-formed typed envelopes through verbatim (no double-wrap), compose <EVENT type="msg" from=…> otherwise, chunk oversized lines into EVENT-PART
2026-09-06T13:09:01.8170614Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8170638Z 
2026-09-06T13:09:01.8170729Z ### REQ-MSG-ENVELOPE
2026-09-06T13:09:01.8174245Z - Title: The <EVENT type="msg" from=…>body</EVENT> envelope (spt-proto::event, the ADR-0001 grammar) is the SOLE canonical arriving-message format at EVERY harness arriving-message surface on an AGENT perch — api listen AND api poll/worker-poll, byte-identical (reverses REQ-MSG-4's 'hook drains keep the raw frame by contract'). SCOPE CARVE-OUT: the shell-command relay (api poll <shell-id> --link, cmd_poll_shell) is a distinct internal transport carrying RAW MAC'd stamped frames the shell child consumes verbatim — NOT an arriving-message surface, deliberately EXEMPT from <EVENT> composition (notify_shell_e2e guards this boundary). __REPLY_TO__ — mis-elevated during the clean-room port to a fake ADR-0001 'stable wire format' (spt-msg/wire.rs, lib.rs) — is REMOVED entirely (spool format_row, the spt-msg TCP frame, emit parse_frame); (from, body) carried structurally, <EVENT> composed once at the delivery boundary. No legacy sister-interop (spt-core never required it). Reply-correlation rebinds onto the structural from / <EVENT from=…> attribute (ADR-0009 access-gate + ADR-0012 Psyche/spt-live reply-target). Self-delimiting by construction → finding F-002 (non-self-delimiting multi-message poll) dissolves. ADR-0020.
2026-09-06T13:09:01.8174463Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8174492Z 
2026-09-06T13:09:01.8174574Z ### REQ-MSG-5
2026-09-06T13:09:01.8175247Z - Title: user-msg envelope kind + daemon identity gate: a Gateway endpoint / the local user's CLI author user-msg (the user's authority); agent-family senders re-stamped to plain msg; identity-gated never payload-trusted (KH 7.3/7.5); wire-additive (N-1 receivers tolerate the new type)
2026-09-06T13:09:01.8175345Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8175374Z 
2026-09-06T13:09:01.8175451Z ### REQ-MSG-6
2026-09-06T13:09:01.8177083Z - Title: cross-node Gateway user-msg honored via advertised endpoint_type: a user-msg from a Gateway-typed origin survives the receive_wan funnel as user-msg (vs the fail-closed re-stamp), keyed on the QUIC-handshake-proven origin node (never wire `from`). Trust boundary = subnet membership (operator-ratified 2026-06-13); no defense against an in-subnet member forging the type. Instance.endpoint_type is an additive serde-default field extending REQ-INST-7's data model. Absent/unknown type → re-stamp (N-1 rollout grace)
2026-09-06T13:09:01.8177231Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8177264Z 
2026-09-06T13:09:01.8177354Z ### REQ-MSG-SENDER-STAMP
2026-09-06T13:09:01.8187943Z - Title: Daemon-stamped authenticated sender: a NEW additive `WanMessage.sender_proven` field (serde-default) carrying the SESSION-PROVEN sender endpoint id, which lights up the REQ-ACL-SUBJECT-CHAIN tier-1 sender-endpoint rule that shipped schema-real but unfed in W1. The stamp is sourced from the session-proven path (`roster::detect_self_id` / the bound perch), NEVER from the caller-supplied `--from`: cli.rs `resolve_from` lets an explicit `--from` win over session detection, which is exactly why KNOWN-HAZARDS 7.5 binds `from` as reply-routing metadata and never an authorization subject. `from` is untouched and keeps its meaning; this is an addition, never a repurposing. TRUST BOUNDARY, stated so no later reader inflates "authenticated sender": the stamp proves the origin NODE cryptographically (QUIC handshake); the endpoint WITHIN that node is asserted by the sending daemon; strength therefore equals REQ-MSG-6's ratified boundary (trust = subnet membership, node = human-proxy). It DEFEATS agents forging `--from` on a box — the adversary milestone A's threat model actually names — and does NOT defend against a malicious member node. Same-node delivery is strictly stronger (the daemon knows the authenticated perch directly). Tier 1 ABSTAINS on absence (no stamp -> None -> the chain continues to the node tier), so N-1 senders, older daemons, and the five gate families that carry no sender endpoint keep today's behavior byte-for-byte. Adapter-invisible: a decision INPUT only, never entering the EVENT envelope, so no published adapter contract changes. Gate: doc — ADR-0009 amended (its "not the sender endpoint's identity" sentence becomes false the moment tier 1 fires) plus the wanmsg.rs module-doc carve-out stating that sender_proven IS decoded-and-acted-on, what bounds it, and that it never becomes the node subject (the origin-node paragraph stays verbatim — origin_node remains never-read-from-bytes, and `forged_origin_field_is_inert` stays untouched); impl — the additive field, the session-proven stamp at the send path, the receive-side threading into `AccessRequest.sender_endpoint`; unit — additive round-trip both directions (new field decodes, absent field defaults), a `--from` that disagrees with the stamp never becomes the subject, tier 1 fires on a proven stamp and abstains without one, and the W1 inertness guards REPLACED by their positive counterparts (the deliberate flip is the record). LATENT-RULE SCAN: REMOVED, AND NO LONGER REQUIRED BY THIS REQUIREMENT (releases#167, operator-ruled 2026-08-19: 'i never asked for it to be added, and never approved of its addition'). It was ruled in on 2026-07-29 as mechanism-not-memory — on the load where the tier went live, count existing SenderEndpoint rules and say loudly, once, that they were inert and are now live — and it found ZERO by construction in the coupled release, since no shipped version accepts a v2 store while the tier is inert. releases#155 asked for that unverifiable history claim to be made honest; the removal DISCHARGES it, because a banner that does not exist cannot claim a node's past or re-fire every boot. THE STAMP ITSELF IS UNTOUCHED: this strikes the announcement, not the tier — the additive wire field, the session-proven stamp, the receive-side threading and every tier-1 decision keep their evidence. The population accessor the scan used (`sender_endpoint_rule_ids`) survives with its other consumer under REQ-UNLISTED-EVIDENCE, which is where its unit now lives. If the DECOUPLING scenario this was written for ever becomes real, it returns as its own item with its own operator decision, never as a residue of this one.
2026-09-06T13:09:01.8188239Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8188263Z 
2026-09-06T13:09:01.8188360Z ### REQ-NODE-IDENTITY
2026-09-06T13:09:01.8188607Z - Title: Ed25519 identity primitive: keypair, detached sign/verify, stable pubkey<->hex
2026-09-06T13:09:01.8188707Z - Required stages: impl, unit
2026-09-06T13:09:01.8188735Z 
2026-09-06T13:09:01.8188816Z ### REQ-NET-1
2026-09-06T13:09:01.8189078Z - Title: WAN messaging first-class, behind default-on net feature flag
2026-09-06T13:09:01.8189172Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8189205Z 
2026-09-06T13:09:01.8189288Z ### REQ-NET-2
2026-09-06T13:09:01.8189445Z - Title: n0 relay default + self-host knob + plain-language disclosure
2026-09-06T13:09:01.8189544Z - Required stages: impl
2026-09-06T13:09:01.8189573Z 
2026-09-06T13:09:01.8189655Z ### REQ-NET-3
2026-09-06T13:09:01.8189802Z - Title: Cross-node Psyche sync over P2P replaces gh-repo-sync
2026-09-06T13:09:01.8189903Z - Required stages: impl, unit
2026-09-06T13:09:01.8189932Z 
2026-09-06T13:09:01.8190022Z ### REQ-PAIR-1
2026-09-06T13:09:01.8190117Z - Title: TOTP-seeded SPAKE2 pairing
2026-09-06T13:09:01.8190217Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8190247Z 
2026-09-06T13:09:01.8190327Z ### REQ-PAIR-2
2026-09-06T13:09:01.8190455Z - Title: Local trust store with TOFU + warn-on-change
2026-09-06T13:09:01.8190537Z - Required stages: 
2026-09-06T13:09:01.8190566Z 
2026-09-06T13:09:01.8190651Z ### REQ-PAIR-3
2026-09-06T13:09:01.8190784Z - Title: Fetch current pairing code from any paired node
2026-09-06T13:09:01.8190881Z - Required stages: impl, unit
2026-09-06T13:09:01.8190909Z 
2026-09-06T13:09:01.8190994Z ### REQ-PAIR-4
2026-09-06T13:09:01.8191094Z - Title: Subnet naming on first pairing
2026-09-06T13:09:01.8191196Z - Required stages: impl, unit
2026-09-06T13:09:01.8191220Z 
2026-09-06T13:09:01.8191304Z ### REQ-PAIR-5
2026-09-06T13:09:01.8191593Z - Title: Multi-subnet pairing: subnet-name discovery input, create-new-names-up-front, rendezvous-token hashing
2026-09-06T13:09:01.8191692Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8191715Z 
2026-09-06T13:09:01.8191801Z ### REQ-PAIR-6
2026-09-06T13:09:01.8192044Z - Title: Elevation-gated per-subnet code fetch (UAC/root or elevated agent; else authenticator app)
2026-09-06T13:09:01.8192140Z - Required stages: impl, unit
2026-09-06T13:09:01.8192164Z 
2026-09-06T13:09:01.8192255Z ### REQ-PAIR-7
2026-09-06T13:09:01.8192397Z - Title: Subnet icon (inline image metadata, GUI-only consumer)
2026-09-06T13:09:01.8192489Z - Required stages: 
2026-09-06T13:09:01.8192517Z 
2026-09-06T13:09:01.8192603Z ### REQ-SUBNET-1
2026-09-06T13:09:01.8192894Z - Title: spt subnet noun namespace: status view (bare + status [NAME] [--nodes]), create (QR/otpauth), show-code; spt pair deleted
2026-09-06T13:09:01.8192993Z - Required stages: impl, unit
2026-09-06T13:09:01.8193021Z 
2026-09-06T13:09:01.8193104Z ### REQ-SUBNET-2
2026-09-06T13:09:01.8193323Z - Title: Guided join e2e: spt subnet join CLI initiator + always-on daemon pairing responder
2026-09-06T13:09:01.8193419Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8193448Z 
2026-09-06T13:09:01.8193589Z ### REQ-SUBNET-3
2026-09-06T13:09:01.8193843Z - Title: Node labels: hostname-default, gossiped, addressable in @node qualifiers (refuse-on-ambiguity)
2026-09-06T13:09:01.8193933Z - Required stages: impl, unit
2026-09-06T13:09:01.8193962Z 
2026-09-06T13:09:01.8194048Z ### REQ-SUBNET-4
2026-09-06T13:09:01.8194313Z - Title: Subnet membership mutations elevation-gated (create = seed reveal; join = trust-boundary enrollment)
2026-09-06T13:09:01.8194409Z - Required stages: impl, unit
2026-09-06T13:09:01.8194437Z 
2026-09-06T13:09:01.8194521Z ### REQ-DOCS-6
2026-09-06T13:09:01.8194817Z - Title: spt how-to <topic>: in-binary task-oriented agent instructions (anti-drift; quickstart prompts point agents at it)
2026-09-06T13:09:01.8195012Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8195041Z 
2026-09-06T13:09:01.8195122Z ### REQ-SEC-1
2026-09-06T13:09:01.8195485Z - Title: Per-endpoint access whitelist: origin-node gate, stateful-firewall (reply/outbound exempt), node-now/user-later, outer gate before grants
2026-09-06T13:09:01.8195636Z - Required stages: impl, unit, doc
2026-09-06T13:09:01.8195661Z 
2026-09-06T13:09:01.8195742Z ### REQ-NOTIF-1
2026-09-06T13:09:01.8196076Z - Title: Notification primitive: per-subnet replicated spool, seen/dismissed, resurface-at-boundary, subsumes update+consent prompts
2026-09-06T13:09:01.8196171Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8196199Z 
2026-09-06T13:09:01.8196286Z ### REQ-NOTIF-2
2026-09-06T13:09:01.8196528Z - Title: spt notify (agent-issued subnet notif) + notif_command manifest seam (harness + shell adapters)
2026-09-06T13:09:01.8196634Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8196668Z 
2026-09-06T13:09:01.8196757Z ### REQ-NOTIF-SCOPE
2026-09-06T13:09:01.8198394Z - Title: Notif scope is a per-row producer-chosen attribute node|subnet: node-scoped rows (update/consent/rollback kinds) live and die on their node and NEVER enter a replication feed; subnet-scoped rows keep the full ADR-0007 machinery (replication, cross-node dismiss, semilattice join); scope determines the first-fire candidate set (node-scoped targets the most-recently-active endpoint ON that node). New row fields additive serde-defaulted (host_binaries pattern) — old peers parse clean, schema regen drift-gated
2026-09-06T13:09:01.8198513Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8198543Z 
2026-09-06T13:09:01.8198633Z ### REQ-NOTIF-COALESCE
2026-09-06T13:09:01.8199819Z - Title: Coalesce-key supersession: producer-stamped key REQUIRED in namespaced form <owner>:<key> (e.g. spt-core:update-staged) — the produce front door rejects un-namespaced keys; producing a new row with the same (scope-target, kind, key) auto-dismisses the prior rows, latest-wins; supersession expresses through the existing dismissed one-way latch (semilattice unchanged)
2026-09-06T13:09:01.8199934Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8199962Z 
2026-09-06T13:09:01.8200080Z ### REQ-NOTIF-SEAM-DISMISS
2026-09-06T13:09:01.8201330Z - Title: Staleness is dismissed at the seam that knows, never evaluated at surface: a successful update apply dismisses spt-core:update-staged; the update worker's next check, seeing running >= staged, dismisses it too (covers out-of-band installs); a later successful update dismisses a rollback row; the primitive stores rows and latches — NO relevance predicates, nothing evaluated at surface time
2026-09-06T13:09:01.8201446Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8201475Z 
2026-09-06T13:09:01.8201568Z ### REQ-NOTIF-QUIET-DELIVERY
2026-09-06T13:09:01.8202667Z - Title: The notify kind rides the active_only window UNCONDITIONALLY, rollback included: spool-only, never live TCP, never a wake, for EVERY producer; loudness = resurface-until-dismissed persistence, never PTY interruption; boundary resurface (clear/compact/new-session/wake) + the adapter safe-point drain are the surfacing paths; a future interrupting alert needs a new kind + its own ADR, not an exception here
2026-09-06T13:09:01.8202777Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8202867Z 
2026-09-06T13:09:01.8202952Z ### REQ-NOTIF-TTL
2026-09-06T13:09:01.8203902Z - Title: Producer-optional TTL for informational kinds with no dismissal seam (node-paired, agent-issued): expired row auto-dismissed instead of surfaced — a timestamp compare, not a predicate; expiry IGNORES seen-state (TTL means stale-after-this; a producer that cannot accept silent expiry must not set one); no global default
2026-09-06T13:09:01.8204003Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8204031Z 
2026-09-06T13:09:01.8204116Z ### REQ-NOTIF-MIGRATE
2026-09-06T13:09:01.8205320Z - Title: One-shot field migration: on first run the new binary auto-dismisses existing rows with from_id = spt-update (kinds consent/rollback) — the known-stale class; idempotent; the dismissals replicate so cleanup reaches not-yet-upgraded peers; agent/psyche rows untouched; the update worker re-produces any genuinely-current update notif within one check cadence
2026-09-06T13:09:01.8205429Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8205509Z 
2026-09-06T13:09:01.8205597Z ### REQ-UPD-1
2026-09-06T13:09:01.8205701Z - Title: Peer-propagated update over P2P
2026-09-06T13:09:01.8205796Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8205824Z 
2026-09-06T13:09:01.8205907Z ### REQ-UPD-2
2026-09-06T13:09:01.8206030Z - Title: All binaries signature-verified before handoff
2026-09-06T13:09:01.8206125Z - Required stages: impl, unit
2026-09-06T13:09:01.8206153Z 
2026-09-06T13:09:01.8206236Z ### REQ-UPD-3
2026-09-06T13:09:01.8206388Z - Title: No endpoint process terminates/suspends during self-update
2026-09-06T13:09:01.8206497Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8206521Z 
2026-09-06T13:09:01.8206603Z ### REQ-UPD-4
2026-09-06T13:09:01.8206769Z - Title: Update gated on user confirmation by default; opt-in full-auto
2026-09-06T13:09:01.8206861Z - Required stages: impl, unit
2026-09-06T13:09:01.8206889Z 
2026-09-06T13:09:01.8206970Z ### REQ-UPD-5
2026-09-06T13:09:01.8207098Z - Title: spt-core ripple-updates registered adapters
2026-09-06T13:09:01.8207185Z - Required stages: impl, unit
2026-09-06T13:09:01.8207213Z 
2026-09-06T13:09:01.8207299Z ### REQ-UPD-6
2026-09-06T13:09:01.8208062Z - Title: Platform-targeted update sets and debug rollout: signed multi-platform update metadata, recipient platform selection, channel-scoped monotonic counters, debug-channel opt-in via release-key overlay, local staging plus pull-based peer propagation, and maintainer-only convergence tooling (ADR-0016)
2026-09-06T13:09:01.8208163Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8208197Z 
2026-09-06T13:09:01.8208278Z ### REQ-UPD-7
2026-09-06T13:09:01.8210307Z - Title: Origin-source update bootstrap (`spt update fetch`): pull the latest signed release directly from the GitHub release origin (`SaberMage/spt-releases`) — the per-platform artifact + its `<asset>.release.json` SignedRelease metadata — and stage it through the EXISTING verify→stage pipeline (the same `plan_verified` gate: two-key signature + channel + monotonic rollback floor + SHA-256), after which the normal consent-notif / `spt update apply` flow is unchanged. Closes the peer-only-discovery gap (REQ-UPD-1): a first-in-fleet / isolated node can update with no peer to pull from. The signed-release anchor keeps the GitHub transport untrusted-but-verified.
2026-09-06T13:09:01.8210412Z - Required stages: impl, unit
2026-09-06T13:09:01.8210442Z 
2026-09-06T13:09:01.8210523Z ### REQ-UPD-8
2026-09-06T13:09:01.8212941Z - Title: Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
2026-09-06T13:09:01.8213108Z - Required stages: impl, unit
2026-09-06T13:09:01.8213136Z 
2026-09-06T13:09:01.8213227Z ### REQ-UPD-9
2026-09-06T13:09:01.8215779Z - Title: `gh_release` adapter [update] avenue (optional signing): an adapter declares `[update] avenue = "gh_release", repo = "user/repo"` (+ optional `asset`, default `adapter.spt`; + optional Ed25519 `signing_key`); spt-core's ripple compares the repo's LATEST GitHub release version against the installed adapter version and, when newer, auto-updates by fetching the release `.spt` archive (the REQ-INSTALL-9 `--release` fetch primitive) → verifies the `.spt` against `signing_key` if declared, else HTTPS+GitHub first-acquisition trust → re-extracts + re-registers the adapter root. Lets a harness adapter ship updates from its own GitHub releases with NO signing tooling or plugin coupling (removes the perri file_pull/delegated avenue blockers). Acquisition-trust mirrors `--release` + the installer first-fetch; does not alter spt-core self-update (REQ-UPD-1..8).
2026-09-06T13:09:01.8216032Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8216060Z 
2026-09-06T13:09:01.8216152Z ### REQ-TERM-1
2026-09-06T13:09:01.8216313Z - Title: Process-supervisor terminal wrapper hosting broker PTYs
2026-09-06T13:09:01.8216408Z - Required stages: impl, unit
2026-09-06T13:09:01.8216442Z 
2026-09-06T13:09:01.8216529Z ### REQ-TERM-2
2026-09-06T13:09:01.8216704Z - Title: session-surface abstraction; send-keys + send-line injection
2026-09-06T13:09:01.8216820Z - Required stages: impl, unit
2026-09-06T13:09:01.8216848Z 
2026-09-06T13:09:01.8216933Z ### REQ-TERM-3
2026-09-06T13:09:01.8217057Z - Title: Byte-stream remote terminal streaming for v1
2026-09-06T13:09:01.8217153Z - Required stages: impl, unit
2026-09-06T13:09:01.8217187Z 
2026-09-06T13:09:01.8217267Z ### REQ-TERM-4
2026-09-06T13:09:01.8217721Z - Title: Live activity buffer (session digest): projection of normalized session logs, snapshot-pull (spt endpoint digest) + structured-delta-stream contract + api digest-entry push
2026-09-06T13:09:01.8217816Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8217845Z 
2026-09-06T13:09:01.8217938Z ### REQ-TERM-5
2026-09-06T13:09:01.8219571Z - Title: Adapter-declared digest extractor seam: a `[digest]` manifest section declaring an imperative extractor (native harness log -> the {role,text,tool,ts} contract; defaults to the [history] source files with an own-source escape hatch), `api digest-entry` push fallback, register-time validation of the section, adapter-declared presentation defaults (window depth, arg-truncation, sprint-collapse) that any consumer may override, and a `spt adapter digest-proof` author tool plus runtime skip-diagnostics (no silent drop). Reverses M9's no-manifest-seam stance; no declarative DSL.
2026-09-06T13:09:01.8219692Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8219720Z 
2026-09-06T13:09:01.8219805Z ### REQ-TERM-6
2026-09-06T13:09:01.8220910Z - Title: Thread-spanning digest across session boundaries: a per-endpoint session ledger (`<perch>/sessions.log`) appended at first bind and by `api boundary` on `/clear`|`/compact` session rotation, the digest enumerating the last K sessions so its rolling window bridges a boundary, and a distinctive in-timeline boundary marker (DigestEntry::Boundary). The digest follows the live-agent thread, not a single session.
2026-09-06T13:09:01.8221021Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8221050Z 
2026-09-06T13:09:01.8221139Z ### REQ-TERM-7
2026-09-06T13:09:01.8224879Z - Title: Two-origin digest merge: spt-owned context-injection entries (psyche_download | echo_commune | owl_message) appended by spt to the endpoint `digest.log`, timestamp-interleaved with the adapter's extracted activity records into one ordered timeline, via a distinct context-injection record category. Data model only this milestone; GUI collapse/expand and the echo-reads-digest delta loop are deferred to the surfaces that consume them. THE MIDDLE SUBTYPE WAS `echo_mirror` UNTIL releases#113, AND THE NAME WAS A PHANTOM: nothing ever mirrored an echo-commune into a running context. run_echo_commune writes the brief as a commune DROP FILE whose ingest routes it into the DURABLE context tiers a later session resumes from; record_context_injection only appends a digest line; ADR-0019 defers the read-back loop. The name plus a comment reading `record that spt mirrored an echo-commune brief into the agent's context` were the ENTIRE basis on which releases#113 reported a behavioural divergence from legacy, and the ticket ordered the removal of a behaviour that was never built. Measured at 343df76e (todlando). Re-kinded to `echo_commune` for what it is -- a record that a brief was PRODUCED for this agent -- and the comment corrected in the same commit, because a name that asserts a behaviour is how the phantom gets re-derived. Distinct namespace from spt_proto EVENT_TYPE_ECHO_COMMUNE and the manifest [session.echo_commune] role, which spell the same word for different things.
2026-09-06T13:09:01.8225180Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8225208Z 
2026-09-06T13:09:01.8225289Z ### REQ-FRONT-1
2026-09-06T13:09:01.8225448Z - Title: Day-one launcher/manager frontend (list/launch/attach/init)
2026-09-06T13:09:01.8225532Z - Required stages: 
2026-09-06T13:09:01.8225560Z 
2026-09-06T13:09:01.8225661Z ### REQ-HOST-RUN-1
2026-09-06T13:09:01.8228010Z - Title: spt-hosted harness bringup: `spt endpoint run` spawns an adapter's `[session.self]` command template into a broker-held PTY (the spawn-session seam, brain.rs spawn_session_pid — same broker path shellhost.rs launch_shell_brokered_in uses for shells, now for kind="harness" self-role), registers the perch under the given endpoint id, returns the id. Reverses today's harness-hosted-only launch (external launcher → `api bind`). Non-interactive flag set (--adapter <a[:profile]> --id <id> --create --resume <session> --attach|--start|--view) covers every terminal action of the W2 interactive picker so shortcuts (cc-<id>) bake fully non-interactive launches; composite adapter:profile resolves via registry::resolve_option leaf-replace overlay.
2026-09-06T13:09:01.8228122Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8228151Z 
2026-09-06T13:09:01.8228237Z ### REQ-RC-1
2026-09-06T13:09:01.8230337Z - Title: `spt rc <id>` — user CLI attaching a local terminal to a broker-held PTY, reusing the cross-node attach machinery (attach.rs request_attach → send_attach_input pump, spt-net AttachRecord codec); local attach is the degenerate single-node case of the cross-node path (rides REQ-TERM-3 byte-stream streaming). Read-only `--view` (watch, no stdin forwarded). Clean detach that does NOT terminate the broker-held session (KNOWN-HAZARDS: PTY ownership stays with the broker; no termination on detach). Explicit detach keybind that cannot collide with harness passthrough input (legacy capsule used a ctrl-b prefix); documented. ConPTY DSR auto-answer in the attach reader (hazard 5.5).
2026-09-06T13:09:01.8230451Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8230475Z 
2026-09-06T13:09:01.8230567Z ### REQ-HOST-RUN-2
2026-09-06T13:09:01.8232262Z - Title: Project-scoped working directory for spt-hosted bringup: an spt-hosted bringup lands the broker-spawned harness PTY in the user's PROJECT cwd, not the daemon's, via an additive `SpawnReq.cwd` field carried through the broker PTY spawn (portable-pty CommandBuilder cwd). N-1-safe wire change (additive, defaulted). Required because the consumer (Claude Code) is project-scoped: broker-inherited cwd = the daemon's cwd = the wrong `.claude`, wrong session history, wrong digest source; `cc <id>` at a project root MUST land the harness in that project. W1 ships broker-inherited cwd as a bringup-proof shortcut only; this REQ must land before the M12 gate (doyle, 2026-06-14).
2026-09-06T13:09:01.8232424Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8232453Z 
2026-09-06T13:09:01.8232539Z ### REQ-RUN-PICKER
2026-09-06T13:09:01.8237081Z - Title: Interactive `spt endpoint run` picker (ratatui TUI): bare `spt` on an interactive terminal enters an in-process picker (flags-present = the REQ-HOST-RUN-1 non-interactive path, untouched). Layer 1 picks kind (Create new | Pick existing). Create-new: choose a registered kind="harness" adapter with its shipped+local profiles tree-nested (registry::registered / manifest.profiles / local_profile_names) → enter a charset-validated id → start. Pick-existing: category select (left/right) over [<cwd-project> | Local node | Subnet], endpoints grouped + alphabetically sorted per category, a status square per endpoint (online green ■ / offline gray ▢ — the blue "attached" tri-state + Kick are DEFERRED to a broker attach-presence slice, M12-W2-RULING Q1), type-to-filter (`/`, nucleo-matcher), a pinned keybind legend, and a right-half two-pane description (harness adapter:profile · best-effort project history newest→oldest from the contextstore p-<project> branches, empty-if-none · `spt endpoint description`). Confirm layer offers status-dependent options — Attach/Start/View (rc pump / cmd_endpoint_run) · Instantiate-locally (remote) · Change-harness-adapter (offline) · Fork (cmd_fork) · Resume-from-history (offline+LOCAL only; enumerate spt_store::sessions::last_k, titles `<project> @ <ts> (…id5)`, feed session_id → cmd_endpoint_run --resume). A single action enum is the source of truth so a future tap-mode (phone PTY) layers on without re-coupling to keybinds. EVERY terminal action routes through cmd_endpoint_run / existing CLI fns — no second bringup path.
2026-09-06T13:09:01.8237339Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8237367Z 
2026-09-06T13:09:01.8237457Z ### REQ-RUN-SHORTCUT
2026-09-06T13:09:01.8242112Z - Title: `<basename>-<id>` launcher shortcut generation (picker `s` keybind, M12-W2-T2.4): from any pre-start options set the picker writes/updates a `<basename>-<id>` launcher at the project root whose body is `spt go <id>` (since U3 it bakes no selection; pre-U3 it baked the non-interactive flags: adapter[:profile] + id + (create|resume) + (start|attach|view); Kick/Instantiate/Change-adapter/Fork are interactive-only, not bakeable). BASENAME IS A PARAMETER (operator rev. 2026-06-14): harness-agnostic spt-core defaults to `spt` (→ `spt-<id>`); an adapter/flow OVERRIDES it (spt-claude-code → `cc`), so spt-core NEVER bakes `cc` (a harness name) into itself. The basename must be a DISTINCT token, never bare `spt` (a `spt.cmd` would shadow the real `spt.exe` only under cmd.exe cwd-first search, silently no-op in PowerShell/Unix, and self-recurse). The script is the CURRENT OS's native form — `.cmd` on Windows (NOT `.ps1`: default PATHEXT excludes `.ps1` so a bare/ext-less name never resolves one; `.cmd` is PATHEXT-resolvable), POSIX `sh` (+chmod +x) on Unix (a single portable form can't be both). The generated header documents the invocation reality (cmd.exe bare `<name>` in the project dir / PowerShell `.\<name>` / Unix `./<name>`; a truly-bare basename on PATH = a PATH-installed launcher, `/spt:setup`'s job). Overwrite is SENTINEL-guarded: the generator writes + checks a generated-by header marker — it overwrites its own prior output freely, but REFUSES + warns if a same-named file lacks the sentinel (never clobber a user file). Requires the additive `--create` flag on `Run{}` (the default-fresh made explicit; N-1-safe).
2026-09-06T13:09:01.8242236Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8242269Z 
2026-09-06T13:09:01.8242359Z ### REQ-RUN-PICKER-HOME
2026-09-06T13:09:01.8245704Z - Title: Home-subnet selection LAYER in the `spt endpoint run` ratatui Create-new picker (v0.14.1; the deferred half of REQ-RUN-MULTISUBNET-HOME's interactive path — ADR-0026 §3 'the interactive picker lists subnets MRU-ordered'). On a MULTI-SUBNET node the Create-new flow gains a `CreateHome` screen (CreateAdapter → CreateId → CreateHome → Confirm) that lists the node's MEMBER subnets MRU-ordered (reusing recent_home::mru_preference + order_by_mru), default cursor = MRU head; the chosen subnet rides Outcome::Run{subnet} into cmd_endpoint_run's --subnet, so decide_run_home resolves Home directly and the post-TUI `Ok to proceed? Y/n` confirm NEVER fires for the picker path. Single-subnet / local-only nodes SKIP the layer (assign_home auto-homes; CreateId → Confirm unchanged). The CLI `endpoint create` path KEEPS the decide_run_home Y/n confirm + the non-interactive MULTI_SUBNET_HOME refuse (operator: the confirm stays useful for CLI-only bringup, just not in the TUI). Esc backs CreateHome → CreateId; Enter selects → Confirm. Pure front-end invariant preserved: the layer only collects --subnet, routes through the one bringup core.
2026-09-06T13:09:01.8245976Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8246004Z 
2026-09-06T13:09:01.8246090Z ### REQ-ELEVATE-1
2026-09-06T13:09:01.8249196Z - Title: Cross-platform self-elevating re-launch for privilege-gated commands: a pure decision seam `decide_elevation_path(os, elevation, interactive_tty, has_display, has_pkexec, has_term_emulator) -> ElevatePath{AlreadyElevated, InlineSudo, UacWindow, Pkexec, TerminalEmulator, PrintHint}` selecting how to re-acquire privilege, and the per-OS impure launchers it dispatches — Windows UAC console (ShellExecuteW `runas` on the abs-exe + verbatim argv; the elevated child does the work, prints 'You can close this window', and pauses for a keypress; the original prints 'Elevated terminal launched…' and exits 0; NEVER pipes the child's stdout back across the privilege boundary), Linux desktop pkexec (preferred, native polkit GUI auth) else x-terminal-emulator -e sudo (fallback list x-terminal-emulator→gnome-terminal→konsole→xterm), the existing interactive-TTY inline sudo, and the headless/no-path floor that prints the absolute-path command. Reused by every gated command (not subnet-specific). Generalizes should_auto_elevate.
2026-09-06T13:09:01.8249372Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8249402Z 
2026-09-06T13:09:01.8249492Z ### REQ-BENCH-STEP-LEDGER
2026-09-06T13:09:01.8254336Z - Title: BUILD-STEP TIMINGS ARE RECORDED AS ONE JSONL ROW SHAPE, READ ON DEMAND, AND GATE NOTHING. Coverage is CI workflow steps AND the local mechanical steps agents invoke on this box (gate-rig prebuilds, wave gate lanes, assembly compile-gate, release packaging) -- the local gate builds are the largest measured wall-clock sink and are invisible to CI timing entirely. Instrumentation lives in the workflow and in xtask timing wraps; it is NOT an spt binary feature. ONE ROW SHAPE: step name, sha or input-key, host, duration, timestamp -- plus an ok flag, because an aggregate that blends a three-second failure with a twenty-minute success under one step name describes neither, so failures are excluded rather than averaged in. TWO WRITERS, ONE FORMAT: the local wrap is xtask, the CI wrap is a shell snippet (the xtask binary cannot time the build that produces it), and a parity test pins the snippet against the single field list, because a format that drifts silently makes the history unreadable at exactly the moment somebody wants a trend. THE LEDGER IS NEVER COMMITTED: CI emits a per-run artifact, local steps append to a gitignored directory; a hand-maintained ledger file in git is the conflict-magnet class every lane would touch. REDUNDANCY IS KEYED ON THE INPUT, never on the run count: two runs of one step at the SAME key is repeated work and two runs at DIFFERENT keys is just work, and a counter that cannot tell them apart flags every honest build as waste; a repeat is reported as a CANDIDATE, since a rerun after a fix is legitimate. WRAPPING IS TRANSPARENT: the wrapped command's stdio is inherited untouched and its exit code is propagated, so instrumenting a step cannot change what that step does or how a caller reads it. Consumption is `xtask bench-report` on demand, read at milestone close-out, findings become filings. NO dashboard and NO per-run regression gate: this box's wall-clock moves with whatever else is building on it, so a threshold would manufacture reds out of load.
2026-09-06T13:09:01.8254609Z - Required stages: impl, unit
2026-09-06T13:09:01.8254637Z 
2026-09-06T13:09:01.8254740Z ### REQ-POOL-FOREIGN-TREE-REFUSED
2026-09-06T13:09:01.8260844Z - Title: A SOURCE TREE MUST NOT SILENTLY BUILD INTO A CARGO TARGET DIRECTORY THAT BELONGS TO ANOTHER SOURCE TREE. Two trees sharing one pool overwrite each other artifacts -- measured, not inferred: two packages of the same name and version built from two DIFFERENT paths resolve to ONE build-script output dir. In the v0.51.0 BAROMETER W1/H1 window that fabricated an E0308 in a gate lane which vanished on re-run, a red belonging to neither tree. THE REFUSAL KEYS ON THE RELATIONSHIP, NEVER ON A PATH: the incident that motivated the filing was reached by CARGO_TARGET_DIR pointed at the primary checkout, a directory no path-shaped rule about .worktrees/ would have covered, and a path rule also leaves the primary checkout target as the one unowned pool every agent can name from memory. THE HARM IS INTERLEAVING, NOT SIMULTANEITY -- cargo already holds an exclusive .cargo-lock for a build duration, so two builds are never literally concurrent; what bit was one lane artifacts being invalidated by another between that lane own steps. So the unit of ownership is the LANE, and a lease keyed on a cargo process would be stale seconds after every claim, making every crossing a takeover and the refusal arm UNREACHABLE -- a gate whose failure signature is silence, refused on that evidence. CLAIM AND ENFORCEMENT ARE SPLIT: the lane claim is written at lane start by the rig and names a long-lived holder (pid plus pid_started_at, because a pid alone is unsound across a reboot and a recycled pid would read as a live lane forever); the build script never claims, only enforces. STALENESS IS PROVEN BY THE PROCESS TABLE WITH THE BIRTH STAMP -- holder absent, or present with a mismatched birth -- and mtime or any derived view never counts; UNKNOWN IS NOT STALE, so an unresolvable holder proceeds with a warning and is neither taken over nor refused. A stale holder is a LOUD TAKEOVER rather than a refusal, because sequential sharing is the cheap correct case and refusing it would demand a pool per lane (30+ worktrees at 7-15 GB measured = 210-450 GB, several times the disk the incident was about). An OWNERLESS pool imprints its first builder and proceeds -- a developer own fresh target is never refused -- while an imprinted pool with NO lane claim refuses a foreign tree and prints the claim command, which is how the discipline gap becomes loud instead of permissive. The escape hatch is an env var and prints EVEN WHEN IT WORKS: a silent override would reproduce the original incident exactly.
2026-09-06T13:09:01.8261016Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8261040Z 
2026-09-06T13:09:01.8261134Z ### REQ-POOL-LANE-IDENTITY
2026-09-06T13:09:01.8265215Z - Title: A LANE'S STATE IS READ FROM ITS GIT IDENTITY, AND THE HOLDER PROCESS IS ADVISORY. MEASURED, not inferred: a sweep of this box's pool claims found 3 of 3 holder processes dead while one of those three lanes was demonstrably LIVE -- the holder was an agent session that had been cleared, re-forked or re-parented, which is routine and says nothing about whether the lane finished. A claim that decides FINISHED from a dead holder hands the pool to a second tree mid-lane, which is releases#103's original interleaving hazard reached through the guard rather than around it. So the claim records the lane's GIT IDENTITY -- its branch and the base sha that branch carried at claim time -- and the finished question is answered by ancestry: the lane is IN FLIGHT while its tip is not contained in the integration head, and SETTLED once it is merged, once its branch is gone, or once its branch no longer contains the claimed base (a branch name re-pointed onto other work is the branch-level twin of a recycled pid, and must not read as the same lane). THE BASE SHA IS ALSO THE REPO-IDENTITY WITNESS: ancestry is read in the ARRIVING tree, and a branch that is simply absent from a DIFFERENT repository would otherwise read as a finished lane, so a claimed base whose object is unknown here yields UNKNOWN rather than SETTLED. PRECEDENCE IS ASYMMETRIC ON PURPOSE: a provably live holder still refuses (it can only ever ADD a refusal), ancestry alone may declare a lane settled, and a claim carrying no git identity at all falls back to the pre-identity holder predicate and says so in the line it prints. A stamp written before this field existed therefore keeps its old behaviour instead of silently changing polarity.
2026-09-06T13:09:01.8265453Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8265481Z 
2026-09-06T13:09:01.8265580Z ### REQ-POOL-CLAIM-TREE-MATCH
2026-09-06T13:09:01.8268083Z - Title: POOL-CLAIM MUST REFUSE BEFORE WRITING WHEN --POOL BELONGS TO A DIFFERENT GIT WORKTREE THAN THE CALLER'S CWD, UNLESS THE CALLER EXPLICITLY OPTS INTO THE SANCTIONED SEQUENTIAL TAKEOVER WITH --foreign-pool. The claim verb harvests owner_tree, branch, and base from cwd; an accidental crossing therefore writes a confidently wrong ownership record and reports success. A deliberate crossing is required by the build guard's own takeover remedy: it remains cwd-authored so the arriving lane becomes the owner, but the flag and diagnostic make that identity source loud. The guard's printed cross-tree pool-claim remedies carry the flag, and an executable test proves the verb admits that exact shape. This validates identity harvest only: pool-claim still writes without adjudicating admission, and every ownership enforcement arm remains in the subsequent build (IR-42). Default-refusal discovery resolves the pool's worktree from its nearest existing ancestor, including before the pool exists. (IR-56)
2026-09-06T13:09:01.8268229Z - Required stages: impl, unit
2026-09-06T13:09:01.8268258Z 
2026-09-06T13:09:01.8268358Z ### REQ-PICK-FIDELITY-AUDIT
2026-09-06T13:09:01.8274317Z - Title: AN ASSEMBLY PICK MUST BE AUDITED FOR FIDELITY ON ITS CHANGED-LINE SET, AND THE COUNT AND DIGEST ARMS MUST BE REPORTED AS SEPARATE VERDICTS BECAUSE THEY FAIL IN OPPOSITE DIRECTIONS. MEASURED on the TURNKEY #212 assembly (IR-57): a cherry-pick's conflict resolution silently dropped exactly two lines and the assembled head did not compile, while every instrument in the assembly path reported success -- the pick completed with no conflict remaining, traceable-reqs check returned 797/797 exit 0 because it parses tags and never invokes the compiler, and the lanes themselves were green and stayed provably clean. Lane-green plus conflict-free is not a claim about the assembled head. The audit compares ONLY the + and - lines of git show --format= for the lane source and for its pick, excluding the +++ and --- file headers: hunk headers and context lines legitimately drift once the head's copy of a file has moved, so a whole-diff comparison returns a confident wrong answer. Of 24 picks on that chain two did not match and they were DIFFERENT CLASSES -- one differed in COUNT (302 lane against 300 pick by the prescribed header-excluding pipeline; the register originally cited 310/308, which is the raw count including the 8 file-header lines of its 4 files, and the delta is 2 either way), the real defect; the other had the SAME count and a different digest, a paragraph the head had already amended for another lane whose merged result correctly carries both lanes' sentences. A count check MISSES the first class entirely and a digest check FLAGS the second as if it were a defect, so neither alone classifies a pick: the verb reports both arms per pick, fails the run on a count mismatch only, and prints the digest-only class loudly as a human read rather than reddening on it, because a check that reds on the measured benign class gets disabled and takes the count arm with it. A pick the audit cannot pair to a lane source is UNAUDITED and exits non-zero in its own class rather than reading as a pass, since a silent skip is the same failure the entry documents wearing a green. Both sides of every comparison are read at ONE layer -- git's own diff output for two commits in one repository -- because a comparison taking one side from the working tree and the other from git's normalized view lies toward MISMATCH in a checkout that carries CRLF against LF blobs, which this one does.
2026-09-06T13:09:01.8274583Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8274612Z 
2026-09-06T13:09:01.8274713Z ### REQ-DISK-FLOOR-PREFLIGHT
2026-09-06T13:09:01.8279164Z - Title: A RIG OR GATE RUN MUST RECORD FREE SPACE AT RUN START, AND REFUSE TO START UNDER A FLOOR. MEASURED (IR-59): a full volume reds as a LINKER defect that names no disk -- LNK1318 Unexpected PDB error with a varying parenthesised code, beside LNK4209 debugging information corrupt, and once as a traceable-reqs check PANIC carrying os error 112, so even a REGISTRY red can be the disk's. Nothing in any of those failure texts says disk, two agents spent hours on a mechanism for one of them, and neither read free space. THE FALSIFIER IS FREE SPACE AT THE TIME OF THE RUN, not the shape of the leg table, and a reading that appears only on refusal leaves every red already in hand uninterpretable -- which is the state the fleet was in, since local gate legs run as plain per-run scripts with no preflight path to amend. So the reading is printed on EVERY run, passing or not, in the golden runner's own key shape (disk preflight: drive= free_bytes= floor_bytes=, and RESOURCE=disk on refusal) so one grep reads a local rig log and a CI log alike, and an optional label attributes readings on a box carrying several lanes. The default floor is the golden runner's 32 GiB, so a local rig and a golden leg refuse at ONE number rather than two; a floor of ZERO is print-only mode for informational call sites and never refuses, which is why the default arm must still refuse the same reading a zero floor admits. A run that starts under the floor produces reds that belong to nothing: they are re-run, never re-read. AND ONE READING IS AN INSTANT, NOT HEADROOM (IR-46): the danger window is the TAIL of a cold build -- the measured instance died linking the last and largest artifact of a 74 GB pool while that same build drained the volume out from under itself, and the reading taken before it looked fine.
2026-09-06T13:09:01.8279331Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8279360Z 
2026-09-06T13:09:01.8279460Z ### REQ-POOL-GC-ORPHAN-RECLAIM
2026-09-06T13:09:01.8284949Z - Title: BUILD-CACHE POOLS ARE RECLAIMABLE ON EVIDENCE, NOT ON AGE OR NAME. Cargo never GCs a target directory and pools OUTLIVE the lanes that made them: this box carried gate-target-h1 at 17.5 GB with ZERO links pointing at it -- unreachable, unowned, and un-reclaimable by any mechanism that existed, because nothing recorded whose it was. The ownership stamp supplies the missing facts, so a sweep can classify rather than guess: IN USE (a lane holder live by the process-table-plus-birth-stamp predicate) is never touched; OWNED (owner tree still on disk, no live lane) is somebody's WARM CACHE and is reclaimed only when explicitly asked; ORPHANED (owner tree gone, or unstamped with nothing linking to it) is the safe row. WHAT MAKES A DIRECTORY A POOL IS CARGO'S OWN CACHEDIR.TAG SIGNATURE, never its name -- a sweep that decided by the word target could delete a source directory somebody happened to call that, and the point of this leg is a reclaim an operator can run without auditing it first. UNSTAMPED DOES NOT MEAN UNWANTED: an unstamped pool with inbound links is a pre-guard pool still in use, and getting that backwards would reclaim a live lane's cache on the very first sweep, since the pools predating the stamp are exactly the ones most likely to be somebody's. UNKNOWN LIVENESS COUNTS AS IN USE here, the opposite polarity from the guard's proceed-with-warning, because the consequences are asymmetric: an unresolvable holder that blocks a reclaim costs disk, while one that permits it costs a lane its cache. THE INBOUND SWEEP COMPLETES BEFORE ANY VERDICT -- a link found after the verdict is a link that did not save the pool -- and path comparison is normalized, since Windows records a junction target with a verbatim device prefix and arbitrary case and a raw compare would report zero inbound links for a pool half the box points at, orphaning every one of them. DRY RUN IS THE DEFAULT: the failure mode of a wrong reclaim is silent, nobody notices a deleted warm cache until the next build takes twenty minutes. Deletion re-asserts real-directory-not-link at the point of removal, because a reparse point removed as a tree takes its TARGET with it, and reports size and free space so the reclaim is a number rather than a claim.
2026-09-06T13:09:01.8285211Z - Required stages: impl, unit
2026-09-06T13:09:01.8285239Z 
2026-09-06T13:09:01.8285340Z ### REQ-POOL-GUARD-NOT-SILENCEABLE
2026-09-06T13:09:01.8288298Z - Title: THE POOL GUARD BUILD SCRIPT MUST EMIT NO `rerun-if` DIRECTIVE, AND A TEST MUST ENFORCE THAT RATHER THAN A COMMENT. MEASURED on a two-tree rig sharing one pool with the trees made BYTE-IDENTICAL (diff -r empty), sequence A B A A B: with no directives the script ran FOUR times (A, B, A, B) and the consecutive A A ran it once; with ONE cargo:rerun-if-changed added it ran ONCE and the foreign tree never ran it again at all. A script with no directives is re-run whenever cargo unit fingerprint changes, and that fingerprint carries CARGO_MANIFEST_DIR -- so it fires on exactly the source-tree ALTERNATION the guard exists to catch, fires even when the differing crate is one the script does not live in (the trigger is the tree PATH, not file content), and does not fire when a tree rebuilds itself. Adding a directive does not slow the guard, it SILENCES it, and only on the crossings that matter. A comment forbidding one is a prohibition, not a boundary -- it depends on the next reader -- so the canary reads the checked-in build script and reds if a directive appears. The canary carries its own POSITIVE CONTROL, because an assertion that no line matches is satisfied equally by a detector that matches nothing, including one broken by a later refactor.
2026-09-06T13:09:01.8288441Z - Required stages: impl, unit
2026-09-06T13:09:01.8288470Z 
2026-09-06T13:09:01.8288616Z ### REQ-POOL-ONE-LIVENESS-PREDICATE
2026-09-06T13:09:01.8291051Z - Title: THERE IS EXACTLY ONE PROCESS-IDENTITY PREDICATE IN THIS WORKSPACE, AND THE BUILD-SCRIPT GUARD USES THAT ONE. A cargo build script sees only [build-dependencies], so the pid-plus-birth-stamp predicate could not be reached where it lived in spt-store::proc. The alternative was a second copy -- a second source under exactly the hazard the first exists for (pid reuse; ADR-0047 decision 1 custody pair test; KNOWN-HAZARDS 7.50/7.51), and a liveness predicate that disagrees with itself across two call sites is worse than either copy alone. So the identity core (process_exists, process_started_at, parent_pid_of, process_identity, ProcIdentity, process_table) moved VERBATIM into a leaf crate below spt-store, and spt-store::proc re-exports every item, leaving every call site, doc link and test untouched. The move is behaviour-preserving by construction: nothing was rewritten, only relocated and re-exported.
2026-09-06T13:09:01.8291155Z - Required stages: impl, unit
2026-09-06T13:09:01.8291183Z 
2026-09-06T13:09:01.8291269Z ### REQ-WHOAMI-1
2026-09-06T13:09:01.8293445Z - Title: The `endpoint list` SELF pin carries the Self endpoint's authored `endpoint description` (info::read_info(...).resources) when present, inline after the liveness state; whoami stays a top-level hot-path verb (parse unchanged, REQ-MSG-9) and renders the same description-carrying SELF pin. HISTORY: originally minted whoami as a thin ALIAS of `spt endpoint list` — that alias premise is SUPERSEDED by REQ-WHOAMI-IDENTITY-ONLY (PROJECT-INDEX W1, 2026-07-15): the alias inherited the list's O(perches x branches) git fanout onto hook paths (the 2026-07-15 message-delivery incident), so whoami is now identity-only over the shared render_self_pin. The pin render + parse evidence here stands; the full-roster surface lives solely on `endpoint list`.
2026-09-06T13:09:01.8293689Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8293712Z 
2026-09-06T13:09:01.8293798Z ### REQ-RCVIEW-1
2026-09-06T13:09:01.8298224Z - Title: Remote-attach controller/viewer model (CONTEXT.md:317): a session's broker OutputLog serves ONE interactive controller (input + EXCLUSIVE PTY resize; its viewport sets the size, sent on attach + every window change via crossterm Event::Resize) plus ANY NUMBER of read-only `--view` attachers (output-only, no input, no resize; client-side letterbox — center+pad when larger, clip+1-line indicator when smaller; only the local ctrl-b d detach chord). Attach intent is three-valued (`Viewer | Control | Take`, wire-default Control): Control to a FREE endpoint becomes controller, Control to a CONTROLLED endpoint is REFUSED with guidance (`--view`/`--take`) — never auto-viewer, never silent-displace. Wire adds (additive, N-1 skip-unknown): `Request.intent`, `Resize{rows,cols}` (controller-only), `Size{rows,cols}` (→viewer), `Displaced{by}` (→displaced controller). The brain-resume cursor (delivered_through, ADR-0018) tracks the CONTROLLER ONLY; viewers replay from their own from_seq and never move it. Dormancy keys on the controller ONLY: controller attach wakes / controller detach goes dormant (even with viewers present); viewer attach/detach is wake-neutral and may watch a dormant endpoint as-is. v1: viewing is gated identically to driving — a viewer runs the same access_check(Unsolicited) as a controller (watching reveals full session contents = a real disclosure); a lighter distinct watch-gate is deferred to cross-subnet/finer-consent (CONTEXT.md:317 'driving ≠ watching' = the future seam).
2026-09-06T13:09:01.8302491Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8302515Z 
2026-09-06T13:09:01.8302633Z ### REQ-KICK-1
2026-09-06T13:09:01.8306579Z - Title: Explicit, loud controller displacement: `spt rc kick <target>` / `--take` (Take intent) kicks the incumbent controller and becomes controller; the displaced controller receives a LOUD `Displaced{by}` notice and is FULLY DETACHED (not demoted to a viewer). A default attach to a controlled endpoint is NEVER a silent displace (it is the Control busy-refusal). An old (N-1) rc omits intent → Control, so it can drive a free endpoint but CANNOT `--take` — it can never silently steal, and gets a clean busy-refusal instead. Taking control rides the same access_check(endpoint, origin, Unsolicited) as a normal control attach (if you may drive, you may take — no elevated kick policy). The picker surfaces 'Kick <node> and attach' (Take) only on a controlled (blue ■) endpoint, via the existing attach dispatch (single-bringup-path: intent is a parameter).
2026-09-06T13:09:01.8306917Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8306941Z 
2026-09-06T13:09:01.8307061Z ### REQ-PICKER-1
2026-09-06T13:09:01.8312051Z - Title: The picker renders a FOUR-state endpoint status (extending the W2 online/offline duality): the list-item square AND a color-coded STATUS line at the top of the pick-existing right-side details both show — gray OFFLINE; green ONLINE (online + PTY-controllable spt-hosted, not controlled); amber 'ONLINE - HARNESS ONLY' (online but NOT broker-PTY-controllable = harness-hosted, no broker PTY seat — today mis-shows green); blue 'ONLINE + CONTROLLED' (online + driven_by.is_some()). Derived on EndpointRow from {offline | controllable | driven_by} with precedence offline→gray, else driven_by→blue, else !controllable→amber, else green (driven_by outranks harness-only; mutually exclusive in practice — a harness-only endpoint has no broker PTY to control). The controllable discriminator is a NEW InfoJson.controllable: Option<bool> (serde-default, N-1-safe), stamped at the establish seam — cmd_listen (harness-hosted relay, no broker PTY) → Some(false); cmd_bind live_agent (spt-hosted broker PTY) → Some(true); absent → not-controllable (amber) default (harness-hosted is the common mis-reported case; one bind self-corrects). Store-projection-only (no live daemon query — doyle ruling). (v0.10.0)
2026-09-06T13:09:01.8312457Z - Required stages: impl, unit
2026-09-06T13:09:01.8312486Z 
2026-09-06T13:09:01.8312580Z ### REQ-PICKER-2
2026-09-06T13:09:01.8314990Z - Title: The picker's project-history loader reads the git-backed context store, not the bare working tree: data.rs project_history_for enumerates an endpoint's projects via the BranchStore branch set (the context store keeps per-project context in git branches — contextstore::project_branch(project_id), checked out to projects/<project>/<id>/ only on-demand) instead of raw std::fs::read_dir over the empty working tree (which returned empty for ALL rows incl wall-a — the operator bug). Ordered newest→oldest by branch commit recency; degrades to empty (informational pane), never fails. (v0.10.0)
2026-09-06T13:09:01.8315118Z - Required stages: impl, unit
2026-09-06T13:09:01.8315147Z 
2026-09-06T13:09:01.8315247Z ### REQ-PICKER-3
2026-09-06T13:09:01.8318315Z - Title: A self-owned subnet row reconciles its status to the LIVE roster: a Subnet-category row whose endpoint_id overlaps a local (is_local) roster id is self-owned (this node hosts it), so its status square is OVERRIDDEN with the live roster status — the WAN registry snapshot (wansend::load_snapshots) is a periodically-advertised, independently-stale projection, while the local roster (p.alive) is ground truth for an endpoint this node hosts. One status square per endpoint (CONTEXT.md:348-350 — nothing licenses opposite squares for one endpoint across its Local vs Subnet listings). A reconcile pass in data.rs after the local_rows + subnet_rows gather; BOTH category listings are preserved (Local + Subnet are legitimately distinct views — you are in your own subnet), only the STATUS is unified. (v0.10.0)
2026-09-06T13:09:01.8318509Z - Required stages: impl, unit
2026-09-06T13:09:01.8318538Z 
2026-09-06T13:09:01.8318643Z ### REQ-PICKER-4
2026-09-06T13:09:01.8320954Z - Title: The picker's Subnet category renders the canonical node LABEL, not bare key-hex: a subnet row's node renders as 'LABEL (keyprefix…)' (e.g. 'HFENDULEAM (bcead52b…)') per CONTEXT.md:650 + Instance.node_label, NOT the raw node key-hex (SPT_DEV:14efb80cb… — a picker-only regression because resource_projection→ResourceRow drops node_label, so data.rs subnet_rows uses the raw row.node). Thread node_label into the picker subnet path (ResourceRow gains node_label, or subnet_rows looks it up via the registry's node_labels) and REUSE the one canonical render (format!("{l} ({}…)", key_prefix) — cli.rs / wansend.rs), never a re-implementation. (v0.10.0)
2026-09-06T13:09:01.8321107Z - Required stages: impl, unit
2026-09-06T13:09:01.8321135Z 
2026-09-06T13:09:01.8321229Z ### REQ-PICKER-5
2026-09-06T13:09:01.8325540Z - Title: `spt endpoint list` (bare/subnet view) renders an ALIGNED table with canonical node labels: cmd_endpoint_list prints subnet rows with `\t` TAB separators (cli.rs:~1651-1662) so variable-width endpoint_ids snap fields to different tab-stops → a RAGGED status column (operator screenshot: X/help statuses misaligned vs rt-*/sptc-*/wall-a); and it calls the node renderer with no label → bare key-hex for every row (SAME ResourceRow-drops-node_label root as REQ-PICKER-4). FIX: max-width per-column padding (mirror render_node_rows' pad, pad by char count not byte len — '…' is multibyte) replacing the tabs, and render the node via the shared node_label_display now that ResourceRow carries node_label (REQ-PICKER-4). Extract a pure row-formatter seam so the alignment+label is unit-testable. ALSO: the bare list is the SUBNET view (a just-run LOCAL perch is invisible cross-subnet until the next advertise tick), so emit a `--local` hint line so a freshly-run endpoint isn't perceived as lost. (v0.10.0; operator-flagged + doyle dispatch 2026-06-17)
2026-09-06T13:09:01.8325841Z - Required stages: impl, unit
2026-09-06T13:09:01.8325870Z 
2026-09-06T13:09:01.8325994Z ### REQ-SEND-SPT-HOSTED
2026-09-06T13:09:01.8331581Z - Title: An inbound `spt send` is DELIVERED to an spt-hosted endpoint (brought up via `spt endpoint run` → `api bind`, broker holds its PTY, NO `api listen` relay). Today cmd_bind→establish_perch (api/startup.rs ~441) writes info.json + ready marker + controllable=Some(true) but registers NO message-listener / NO address, so deliver.rs resolve_address→None→spool (deliver.rs:132-140) and the message NEVER reaches the live PTY — the endpoint reads 'online' (ready marker) yet `spt send` silently SPOOLS ('online but not deliverable' lie). Per CONTEXT:187-188 the daemon owns the PTY and delivers, manifest-configurable per activity-state (direct PTY injection / relay / HTTP). FIX: route an inbound send for an spt-hosted target through the daemon → broker InputReq → session.write_input PTY-inject (broker.rs dispatch_input/write_input ~988-1022), the same path the brain uses; the live-delivery handshake must report Sent (not Queued) and stop the spool-only fallback for a broker-hosted, PTY-resident endpoint. Detection is local: controllable==Some(true) + spt-hosted state + resolve_address==None. = the spt-core HALF of the wall-b finding (perri owns the adapter half: bind-hook fired-zero-perch + the missing endpoint-run int test). (post-v0.10.0)
2026-09-06T13:09:01.8331846Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8331878Z 
2026-09-06T13:09:01.8331994Z ### REQ-HAZARD-RC-EOF
2026-09-06T13:09:01.8336183Z - Title: A severed broker stream during a live rc session surfaces GRACEFULLY, never as a raw io error that crashes the PTY. The rc read-loop (rc.rs:352-362) continues only on WouldBlock/TimedOut; ANY other read_event_until error — including UnexpectedEof 'failed to fill whole buffer' — returns Err → RC_FAIL → the PTY 'crashes' from the user's view. Confirmed trigger: a deliberate `spt daemon stop` (broker bounce) severs an active rc (perri stopped the daemon to release owlery watch handles). Same severed-broker-stream EOF class as the v0.9.1 seed fix (seed_fail_message) and the listener-death case — spt-core must classify a broker-gone EOF and (a) surface a CLEAR actionable message ('daemon stopped/restarted — re-run / reconnect'), never the raw buffer error, and ideally (b) AUTO-REATTACH to the same session on the fresh broker (the broker is the daemon-lifetime anchor; it returns on the next `spt api` call). FOLD two side-observations: (1) `spt daemon stop` SILENTLY drops active rc/live sessions — warn ('N active session(s) will drop') or graceful-detach on stop; (2) the daemon holds owlery WATCH HANDLES on perch dirs so a torn-down perch dir stays 'Device busy' until a full daemon stop releases them (perri's rt-* cleanup) — a torn-down perch's handle should release without a daemon stop. doyle Finding C, root-caused. (post-v0.10.0)
2026-09-06T13:09:01.8336320Z - Required stages: impl, unit
2026-09-06T13:09:01.8336348Z 
2026-09-06T13:09:01.8336445Z ### REQ-HAZARD-DEFERRED-MANIFEST
2026-09-06T13:09:01.8338775Z - Title: A pointer-mode (delegated / GhReleaseManaged) adapter whose binary/manifest is not yet extracted is reported with a CLEAR diagnostic, never silently dropped. Today such an adapter reads its manifest LIVE from source_dir (registry.rs manifest_dir ~146/149); a deferred / un-extracted install makes load_manifest fail → registered() (~410, filter_map(.ok())) SILENTLY DROPS the row → downstream ADAPTER_UNRESOLVED + a cryptic os-error-2 on `spt adapter use`. FIX: surface a clear diagnostic at the resolver + at `adapter use` (name the adapter + the deferred/missing-manifest cause + the fix), not a silent filter-drop and not a bare os-error-2; consider an eager manifest copy at register time so host_binaries survive before the binary download completes. doyle Finding A. (post-v0.10.0)
2026-09-06T13:09:01.8339076Z - Required stages: impl, unit
2026-09-06T13:09:01.8339108Z 
2026-09-06T13:09:01.8339212Z ### REQ-HAZARD-ENV-SUBST
2026-09-06T13:09:01.8342559Z - Title: `spt endpoint run` HONORS manifest [env.<VAR>] direction=inject values (with {key} substitution) on the spt-hosted spawn. Today only the [session.self] command ARGV is {id}-substituted; the [env] inject value is NEITHER substituted NOR applied — manifest.schema.json promises EnvVar.value = 'Value to inject (with substitution)' but prepare_harness_spawn fills only argv and SpawnReq carries no env, so a [env.SPT_ENDPOINT_ID].value='{id}' arrives EMPTY. A FLAGLESS harness (bare `claude`, no argv slot for {id}) then routes the id via [env] → empty → SessionStart sees empty $SPT_ENDPOINT_ID → seeds-by-PPID instead of binding → ZERO perch → NO_PERCH (the actual wall-b bind blocker; perri hard-repro'd). SILENT failure (empty inject, no error). FIX (doyle ruled a): fill every [env] inject value from the SAME {key} catalog as argv/role (mirror F-009 TEMPLATE fill, whole-string fill_template for an env value), thread it through SpawnReq.env → the broker sets it on the spawned PTY child. Correctness fix — schema already promises it, NO manifest change, NO new binary. PAIRS with REQ-SEND-SPT-HOSTED to make endpoint run fully work. doyle F-013. (post-v0.10.0)
2026-09-06T13:09:01.8342758Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8342839Z 
2026-09-06T13:09:01.8342936Z ### REQ-HAZARD-ROSTER-GHOST
2026-09-06T13:09:01.8345283Z - Title: A LOCAL subnet roster entry whose backing perch is erased does NOT keep advertising Active (no phantom perch-less endpoint). `api session-end <id> --erase` removes the perch (owlery dir gone) but the subnet roster (identity/registry/<subnet>.json) keeps the endpoint's instance row ACTIVE with no backing perch; `endpoint stop` says 'address unregistered' yet the line persists; no CLI verb forgets a roster entry, and a hand-edit is re-added by the single-writer daemon advertiser. FIX: daemon-side self-heal — the advertiser DROPS/forgets a LOCAL roster entry whose backing perch no longer exists (stops advertising it Active), and/or a `forget`/evict verb; verify whether the epoch lease eventually evicts it (slow-self-heal) vs a real leak and scope accordingly. doyle secondary finding (perri). (post-v0.10.0)
2026-09-06T13:09:01.8345392Z - Required stages: impl, unit
2026-09-06T13:09:01.8345421Z 
2026-09-06T13:09:01.8345526Z ### REQ-WAN-SPT-HOSTED-DELIVERY
2026-09-06T13:09:01.8349163Z - Title: A WAN-ARRIVED `spt send` is DELIVERED to an spt-hosted endpoint (broker holds its PTY, NO api-listen relay), not spooled-forever. Today receive_wan (spt-daemon/wan.rs:271-276) tries deliver_tcp (the harness-hosted relay leg) then falls to spool — it has NO spt-hosted broker-inject leg, which exists ONLY in local cmd_send (REQ-SEND-SPT-HOSTED, Brain::inject_endpoint → KIND_ENDPOINT_INPUT → broker dispatch_endpoint_input → translation-binary idle-inject). So a WAN arrival to an idle spt-hosted perch with a live translation binary ALWAYS sleeps in spool until an adapter hook polls (F-023: perch verifiably idle 7min, binary healthy, zero injection). FIX: factor cmd_send's spt-hosted delivery leg into a SHARED fn; receive_wan calls it after the replay-check (wan_seen_at) + restamp (restamp_wan_user_msg), BEFORE the spool fallback. Claim discipline UNCHANGED: inject delivered=true → wan_mark_seen_at then return the existing 'delivered' wire token (no wire change); delivered=false → the existing spool-with-claim transaction. v0.14.3 LAW: the shared leg is translation-binary-ONLY, NO raw-PTY fallback — a no-binary arrival SPOOLS LOUD, never writes the PTY. (F-023, BUILD-F023-WANIDLE)
2026-09-06T13:09:01.8349283Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8349312Z 
2026-09-06T13:09:01.8349401Z ### REQ-MSG-IDLE-EDGE-DRAIN
2026-09-06T13:09:01.8353491Z - Title: On an endpoint's ACTIVE→IDLE transition the daemon DRAINS its pending spool (NON-DEFERRED ONLY — AMENDED 2026-07-26) through the same shared spt-hosted inject leg — closing the SECOND F-023 gap for the classes an inject may carry: an spt-hosted endpoint (no api-listen relay to wake it) strands its 'IDLE+no-binary → non-deferred for a relay that does not exist' class without this drain. AMENDMENT 2026-07-26 (FIELD-TRUTH W1 roll-in, operator ruling; doyle spool-audit RCA — spt-shells shell-context rows on a LIVE-AGENT perch showed taken_leg=idle-inject ~200ms after boundary spooling, each injection STARTING A TURN): the deferred (active_only) class is EXCLUDED from the idle-edge and parked-re-offer claims UNCONDITIONALLY. active_only means what ADR-0028 says — 'active hook window only; never wakes an idle agent' — so the hook poll is its SOLE carrier on every endpoint class; a deferred row with no hook cadence WAITS (starves-by-design), it is never injected. This knowingly revokes the original F-023 leg-2 rescue of the deferred class (that rescue was the operator-visible defect: background context starting turns). Mechanics unchanged for non-deferred: shared inject leg, REUSE of the hook-poll take/ack (ONE drain path, TWO triggers), v0.14.3 LAW on both triggers (translation-binary-ONLY, no-binary drain SPOOLS LOUD, never writes the PTY). (F-023 as amended; kin REQ-SEND-WINDOW-DRAIN-HONOR)
2026-09-06T13:09:01.8353763Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8353791Z 
2026-09-06T13:09:01.8353896Z ### REQ-HAZARD-DELIVERY-STARVATION
2026-09-06T13:09:01.8356993Z - Title: A NON-DEFERRED message that has REACHED a node's spool (WAN-arrived or locally spooled-while-active) is NEVER dependent on an adapter HOOK-POLL cadence for its eventual delivery to an spt-hosted (relay-less) endpoint — the daemon itself drives delivery on the events it owns (WAN ingress + the ACTIVE→IDLE edge). Hazard class: delivery starvation. Without this, cross-node and post-active messages to an spt-hosted perch strand indefinitely whenever the adapter's hooks are quiet (idle session, no user turns), presenting as 'sent but never lands' with a healthy binary and an idle perch (F-023). SCOPE NARROWED 2026-07-26 (FIELD-TRUTH W1 roll-in, operator ruling): the deferred (active_only) class is EXPLICITLY OUTSIDE this guarantee — per ADR-0028 it is hook-carried background context that never wakes an idle agent; with no hook cadence it waits by design. The no-starvation guarantee binds the default and idle_only classes only. Guarded by REQ-WAN-SPT-HOSTED-DELIVERY (WAN ingress leg) + REQ-MSG-IDLE-EDGE-DRAIN (idle-edge drain, as amended). (F-023)
2026-09-06T13:09:01.8357159Z - Required stages: doc, int
2026-09-06T13:09:01.8357187Z 
2026-09-06T13:09:01.8357279Z ### REQ-MSG-CLI-ORIGIN
2026-09-06T13:09:01.8359209Z - Title: A bare non-perch CLI `spt send` (no owning perch to name as origin) stamps from = `cli@<node-label>` at compose time (bare `cli` when no node label is known — never a dangling `cli@`), and WAN ingress renders an EMPTY from as the origin node DISPLAY (`node_label_display(origin_node, None)` = the QUIC-proven origin node's key-prefix; never blank) — a delivered message NEVER shows a blank sender. Scoped to `spt send`: a from-less send is LEGAL (stamped, never refused), while `spt ring` keeps its NO_SELF refusal (a ring needs a routable self for the reply leg; `cli@<node>` is a display origin, not a perch address). (F-024C item 3, doyle ruled)
2026-09-06T13:09:01.8359317Z - Required stages: impl, unit
2026-09-06T13:09:01.8359347Z 
2026-09-06T13:09:01.8359470Z ### REQ-HAZARD-SESSION-PIN-WEDGE
2026-09-06T13:09:01.8366050Z - Title: A perch PINNED to a DEAD session-id self-heals instead of wedging forever. authenticate() (spt/src/api/auth.rs:78) gates api poll/state/boundary on proof-sid == info.json.session_id; if ONE boundary rotation is lost (transient env corruption kills the /clear-era hook), the perch stays pinned to the dead sid and EVERY id-scoped hook call refuses — INCLUDING boundary itself (it presents the new sid), a permanent strand (ready:false, stale .idle, drain no-ops, WAN spool sleeps forever; AUTH_REFUSED is stderr-only = invisible inside a hook). FIX: authenticate() gains a DEAD-OWNER fallback — when the sid MISMATCHES AND the perch's recorded pid is dead (proc::is_process_alive==false), ACCEPT the caller's sid and RE-PIN (rotate session_id + log SESSION_REPIN loud). Same trust model as establish_perch's conflict gate (api/startup.rs:207-210), which already allows rebind exactly when owner_alive==false (an orphaned perch accepts a new LOCAL owner). A LIVE-owner mismatch STILL refuses (squat protection UNCHANGED). ADDITIVE to token auth — the existing token-auth recovery path is UNTOUCHED; the new branch fires only on (no token) AND (sid mismatch) AND (owner dead). COVERAGE SPLIT (explicit, perri clean-room 2026-07-02 — the wedge latches on /clear even in a CLEAN env, so the corruption domino was sufficient but NOT necessary): the dead-owner re-pin rescues CRASHED/DEAD sessions ONLY; a LIVE-pid rotation (/clear, /compact — same process, new sid) is CORRECTLY refused without the departed session's prior-sid proof and MUST NOT be widened to live owners. The live-rotation contract is adapter-side: the adapter PERSISTS the prior sid across rotation and PRESENTS it as boundary proof (perri's state-file pattern = the reference); a silent boundary skip on an unresolvable id, or a boundary call with NO auth proof, strands the perch (perri's court). Core rescues only the dead-owner orphan; live-rotation proof is the harness-contract's job (see the harness-contract boundary section, which cross-refs this hazard). PARKED (not this wave, logged): pid-ancestry self-proving rotation (core walks the caller's real ancestry vs info.json.pid) — needs an ADR + Windows parent-spoof caveats. (F-024C, F024C-AUTHWEDGE-ADDENDUM + F024D-DOCSCOPE)
2026-09-06T13:09:01.8366365Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8366393Z 
2026-09-06T13:09:01.8366493Z ### REQ-HAZARD-STORE-INIT-RACE
2026-09-06T13:09:01.8369539Z - Title: Concurrent first-touch of ONE fresh BranchStore must ALL succeed, never a hard error. BranchStore::open_or_init (spt-store/src/branchstore.rs:47) is a TOCTOU: it gates on HEAD.exists() then runs a NON-ATOMIC init (`git init --bare` + `git config core.autocrlf false` + best-effort worktree.useRelativePaths). Two processes that both observe !HEAD.exists() on one fresh store race the `git config` step, which takes git's per-repo config.lock — the loser fails with 'could not lock config file …/config: File exists', an io::Error that strands the caller (the G3-gate pump.rs:442 flake, doyle-ledgered). FIX: make init race-tolerant — `git init --bare` is idempotent, and `git config` is idempotent (same bytes), so tolerate a concurrent winner (open-after-lose: if init errors but HEAD now exists, proceed as opened) and retry a transient config.lock collision a bounded number of times so the required core.autocrlf=false is guaranteed set. N concurrent open_or_init on ONE fresh dir must ALL return Ok. (F-025 wave, doyle Item 2)
2026-09-06T13:09:01.8369658Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8369687Z 
2026-09-06T13:09:01.8369802Z ### REQ-HAZARD-SPOOL-SENTINEL-CREATE-FAIL
2026-09-06T13:09:01.8371668Z - Title: A persistent failure to (re)create the spool has-messages sentinel is SURFACED, never silently swallowed. touch_has_messages (spt-store/src/spool.rs:147) does `let _ = File::create(...)` — a live field defect on ENLYZEAM left a stale .has-messages (2026-06-29) beside a fresh spool.db insert (06:59:17Z) in ONE directory, i.e. the create silently failed while rows accumulated (suspected read-only-attrib / share-lock). FIX: on File::create failure emit a LOUD-ONCE-per-perch stderr diagnostic naming the concrete io::Error (self-identifying regardless of kind); do NOT make it fatal (spool writes still proceed). (F-024C item 2, doyle)
2026-09-06T13:09:01.8371772Z - Required stages: impl, unit
2026-09-06T13:09:01.8371802Z 
2026-09-06T13:09:01.8371896Z ### REQ-MANIFEST-NODE-KEY
2026-09-06T13:09:01.8376104Z - Title: A new session-scoped manifest fill key `{node}` resolves to THIS node's advertised label — available wherever the session-scoped keys ({id}/{session_id}/{session_name}) populate: BOTH topologies' spawn-prep catalogs (harnesshost.rs:111-118 self-spawn guaranteed-fill + lifecycle.rs:280 base lifecycle keys, at minimum [session.self] and [session.resume]) AND lazy [strings] eligibility (ADR-0029 family). VALUE (design-true per CONTEXT §node label / REQ-SUBNET-3): the node's ADVERTISED LABEL — the same value node_label_display renders — read from the label store (NodeLabel, registry.rs:118/220, OS-hostname default re-checked at daemon startup), NOT the pubkey and NOT a fresh gethostname at fill time when the store already holds the refreshed label; fall back to the OS hostname only if no label is known. perri's concrete use: templating `--remote-control {id}--{node}` in the claude-spt launch/resume commands. CAVEAT (documented in the manifest.md key-table row AND here): SINGLE-TOKEN fills only — tokenize-then-fill (REQ post-F-009) cannot produce a space-carrying argv element, so composite display names like `<id> @ <node>` remain adapter-shim territory (claude-spt v0.10.3's launch shim stays the reference for those); {node} COMPLEMENTS the shim for tokenizable args, it does not replace it. Origin: perri fill-catalog-gap finding 2026-07-02, operator-promoted into BUILD-F023-WANIDLE (additive, independent of the delivery legs). (NODEKEY-FOLD)
2026-09-06T13:09:01.8376383Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8376412Z 
2026-09-06T13:09:01.8376517Z ### REQ-HAZARD-HOSTED-LIVENESS-RECONCILE
2026-09-06T13:09:01.8380630Z - Title: B2 KEYSTONE: a daemon-hosted (spt-hosted) endpoint's info.json status is RECONCILED to real liveness, not left latched online. The broker exit-waiter (broker.rs:889-910) reaps its in-mem session table + emits ExitEvent but NEVER touches info.json; lifecycle::mark_offline only fires on Psyche teardown — so a dead/exited harness (operator closed the tab) stays status=online forever (is_perch_alive returns ONLINE for daemon-hosted, liveness.rs:80-93). FIX (doyle ruled PULL-PRIMARY — the live-status analog of REQ-HAZARD-ROSTER-GHOST): the livehost reconcile loop (reconcile_once livehost.rs:226-313) queries the broker's live session set (KIND_SESSIONS) each tick and, for any status=online live_agent perch PAST the boot grace whose endpoint has NO live broker session, marks it offline (lifecycle::mark_offline → status=offline → is_perch_alive=false). GATED on spt-hosted (controllable==Some(true)) so a HARNESS-HOSTED relay live agent (api listen, legitimately online with no broker session) is NEVER mis-marked. Crash-robust + self-healing on the next tick (clear-on-event is not crash-robust alone). PUSH (brain ExitEvent→mark_offline) is an OPTIONAL fast-path only if the daemon brain is reliably subscribed to all hosted sessions; correctness rides the pull. Broker stays stateless (ADR-0004 §B — brain owns the info.json write). (v0.12.0)
2026-09-06T13:09:01.8380753Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8380781Z 
2026-09-06T13:09:01.8380891Z ### REQ-HAZARD-RC-ATTACH-FAILFAST
2026-09-06T13:09:01.8384117Z - Title: B1: `spt rc <id>` to a DEAD or non-streaming session fails fast with a clear message, never an INFINITE blank screen. Today rc.rs run_attach (209-231) + pump spawns PUMP_IPC_READER and blocks: the poll times out each slice but the stream never produces output, so the operator sees a permanent blank (operator: fresh wall-f attached, closed tab, then `spt rc wall-f` HUNG — the broker still resolved a session for it). FIX: (a) once B2 lands, gate attach on is_online/status — an offline endpoint yields a clean 'endpoint offline, start it' not an attach; (b) fail-fast — if the attach-open ack / first output does not arrive within a bound, surface a clear message, never an infinite blank; (c) the broker EOFs the attach stream when the session's child is dead, so rc's existing PumpEnd::BrokerGone graceful path (REQ-HAZARD-RC-EOF) catches it. PIN the exact sub-mechanism with a repro test FIRST (dead-session-lingers-in-broker vs reaped-but-rc-waits vs alive-resting-no-wake — the wall-f Windows tab-close: child alive-silent vs dead-not-reaped). (v0.12.0)
2026-09-06T13:09:01.8384353Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8384382Z 
2026-09-06T13:09:01.8384482Z ### REQ-ENDPOINT-STOP-OFFLINE
2026-09-06T13:09:01.8385773Z - Title: H3: `spt endpoint stop <id>` marks the endpoint OFFLINE (alive=false), not merely de-readied. cmd_stop (cli.rs:2994-3010) removes the ready marker + unregisters the address but does NOT set status offline, so a stopped daemon-hosted endpoint still reports alive=true (status=online latch). FIX: add set_status(perch, STATUS_OFFLINE) to cmd_stop — folds with B2 (same setter). Unit: stop → is_perch_alive=false / alive=false. (v0.12.0)
2026-09-06T13:09:01.8385920Z - Required stages: impl, unit
2026-09-06T13:09:01.8385948Z 
2026-09-06T13:09:01.8386049Z ### REQ-HAZARD-DAEMON-STOP-BARRIER
2026-09-06T13:09:01.8387781Z - Title: B3: `spt daemon stop` then an immediate `spt daemon start` does NOT race — stop fully completes before it returns. Today request_stop (seedmap.rs:240-255) returns on the KIND_STOPPING ack (sent seedmap.rs:174-176) BEFORE the seed socket unbinds, so a following is_running ping (daemon.rs:375) wins the exit window and start reports ALREADY_RUNNING (operator: daemon stop → STOPPED then start → ALREADY_RUNNING). FIX: unbind/stop-gate the seed socket BEFORE acking KIND_STOPPING, OR request_stop waits for a ping-to-fail before returning. Unit: stop then immediate is_running()==false. (v0.12.0)
2026-09-06T13:09:01.8387895Z - Required stages: impl, unit
2026-09-06T13:09:01.8387972Z 
2026-09-06T13:09:01.8388086Z ### REQ-HAZARD-SEEDMAP-CONNECT-UNBOUNDED
2026-09-06T13:09:01.8391562Z - Title: SEED (doyle-filed, 2026-07-05, from the REQ-HAZARD-DAEMON-STOP-BARRIER B2 fix): the PRODUCTION seedmap connect callers (put / take / is_running) inherit the SAME interprocess WaitNamedPipeW-forever hazard the stop path just bounded — a Windows named-pipe connect to a name that EXISTS but has NO accepting instance parks in NMPWAIT_WAIT_FOREVER, so a slow / half-dead seed daemon could wedge a live `api seed` / `api listen` / `daemon start`. UPDATE (2026-07-05, doyle reversed the stop-path scope-guard): request_stop's OWN initial connect became load-bearing (a stop-guard re-dialing an already-dying name parked forever, resurrecting the convoy) → it is now bounded via connect_bounded under REQ-HAZARD-DAEMON-STOP-BARRIER (every dial on the STOP path is bounded). REMAINING deferred here = the put / take / is_running production clients. FIX (deferred, needs its own ruling): a shared bounded seed-control connect for those — but a 2s-style cap on a legitimately slow daemon-start connect is a real behavior change (a slow-but-fine start could become a spurious failure), so the timeout + degrade semantics need design first. NOT built — activate when scoped.
2026-09-06T13:09:01.8391679Z - Required stages: 
2026-09-06T13:09:01.8391704Z 
2026-09-06T13:09:01.8391804Z ### REQ-HAZARD-DAEMON-STOP-REAP
2026-09-06T13:09:01.8393555Z - Title: Breap: `spt daemon stop` REAPS the spt-hosted children it spawned — no orphaned psyche/harness processes. Today a stop leaves ~8 orphaned claude-spt-psyche.exe + spt.exe: Psyches are spawned DETACHED (runtime.rs:342-356, the Child is dropped — 'Detached' ~349) and the livehost stop flag Arc<AtomicBool> is NEVER raised (brainproc.rs:227-230 holds it 'for symmetry'). FIX: on stop, raise the livehost stop flag AND kill the spawned psyche/spt-hosted children — via a Windows job object / Unix process-group so the children die with the daemon (not detached-immortal). Folds with B3 (both the stop path). (v0.12.0)
2026-09-06T13:09:01.8393664Z - Required stages: impl, unit
2026-09-06T13:09:01.8393694Z 
2026-09-06T13:09:01.8393804Z ### REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE
2026-09-06T13:09:01.8396147Z - Title: B5: `spt daemon start` does NOT revive phantom Psyches for dead-but-online-latched perches. Today reconcile_once (livehost.rs:285) spawns a Psyche per status=online live_agent perch at boot WITHOUT verifying the harness child / {id}-psyche is actually alive — so a Cold start after an unclean stop revives N psyches for N dead-but-latched perches (3 psyches for 3 dead perches). FIX: gate the boot psyche-spawn on real child-liveness — a perch with NO live broker session (the B2 reconcile signal) is marked OFFLINE at boot instead of hosted, so a dead-harness perch is never revived. Shares the B2 reconcile loop (this is its boot-gate arm); composes with B2's honest latch. Also closes wall-a's psyche_host_error gap (residency-confirm does not run at boot tick-1, livehost.rs:395-441 / 257-263). (v0.12.0)
2026-09-06T13:09:01.8396395Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8396422Z 
2026-09-06T13:09:01.8396546Z ### REQ-HAZARD-BRAIN-RESTART-LIFECYCLE-REHYDRATE
2026-09-06T13:09:01.8399252Z - Title: B4 (deepest): a bare brain restart (broker survives) REHYDRATES the live-agent lifecycle so post-restart endpoints are hosted + attachable. Today resume_sessions (brainproc.rs:186, brain.rs:797-809) re-subscribes to the broker's PTY sessions but ALL BrainLifecycle instances (lifecycle.rs:58-130; the ephemeral brain.rs:254-275) are LOST on restart → a post-restart live endpoint gets no livehost → its Psyche is never (re)hosted and new spawns die / can't attach until a FULL daemon reset (operator: perri's brain kill+restart wedged everything until a full daemon kill). FIX: on brain startup, rebuild a BrainLifecycle per resumed live-capable session — load the manifest from the adapter registry → instantiate → start the pulse — the rehydrate the resume no-op cannot do. Composes with B2 (the reconcile re-hosts from the honest on-disk status after rehydrate). (v0.12.0)
2026-09-06T13:09:01.8399510Z - Required stages: 
2026-09-06T13:09:01.8399539Z 
2026-09-06T13:09:01.8399651Z ### REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP
2026-09-06T13:09:01.8403472Z - Title: A bare brain restart leaves EXACTLY ONE `{id}-psyche` process per endpoint — no duplicate. On an abrupt brain death stop_host never runs (the LiveSet + owned child handles die with the brain) and Breap's job/group only reaps at DAEMON stop, so the PRIOR brain's Psyche stays ALIVE; the respawned brain's reconcile re-hosts a SECOND Psyche and overwrites the `{id}-psyche` perch pid, leaving the old one untracked + alive = a duplicate that lingers until daemon-stop (the operator's 'brain kill+restart wedged everything'). FIX: at brain start, BEFORE the first reconcile re-hosts, reap any pre-existing `{id}-psyche` orphan — ID-SPECIFICALLY (recycle-safe on the shared box, where sibling agents share the `claude` basename): scoped-kill the recorded pid ONLY IF it is alive AND its exe basename == the adapter's psyche program (normalize_basename) AND its COMMAND LINE contains the full psyche id `<id>-psyche` (baked via {id}); a sibling never carries THIS id, and any unreadable signal FAILS SAFE (decline to reap — a missed dup is bounded by Breap, a wrong-kill is catastrophic). CAVEAT: the cmdline carries `<id>-psyche` only when the adapter's psyche_init.command uses {id} (the norm); a non-{id} adapter safely MISSES the reap (today's behavior, Breap bounds it) — never a wrong-kill. (v0.12.0)
2026-09-06T13:09:01.8403590Z - Required stages: 
2026-09-06T13:09:01.8403619Z 
2026-09-06T13:09:01.8403720Z ### REQ-HAZARD-UNHOST-PSYCHE-REAP
2026-09-06T13:09:01.8406821Z - Title: On un-host, the detached `{id}-psyche` HARNESS PROCESS is reaped — not just its in-brain pulse-driver thread. Today stop_host (livehost.rs:203) trips the HostedLife stop flag + JOINS the driver thread, but the Psyche is a detached harness process (spawn_psyche → ManifestRuntime detached spawn, runtime.rs:341-356; its pid is untracked in HostedLife though stamped on the `{id}-psyche` perch, where residency-confirm already reads it). So endpoint-stop / mid-life agent-death / a B2/B5 offline-then-unhost leaves the psyche process ORPHANED, alive until the next daemon-stop (where Breap's job/group reaps the whole brain subtree). The Psyche STAYS a harness process by design (CONTEXT.md 97/203/251 — headless harness session, its own perch) — the fix does NOT move it in-brain; it SCOPED-kills the `{id}-psyche` pid on un-host (never machine-wide — shared box). Track the pid in HostedLife at host_one (cleanest) or read the `{id}-psyche` perch pid at stop_host. Composes with H3 (endpoint stop → offline → reconcile un-host → reap) and B2/B5 (the offline arms that trigger un-host). (v0.12.0)
2026-09-06T13:09:01.8407036Z - Required stages: 
2026-09-06T13:09:01.8407064Z 
2026-09-06T13:09:01.8407154Z ### REQ-ENDPOINT-PURGE
2026-09-06T13:09:01.8411865Z - Title: `spt endpoint purge <id>` fully removes an endpoint AND every record keyed on it — the formal teardown devs/CI need for clean test setup/reset. NOT consent-gated (a local dev/test op — no peer consent). OFFLINE-ONLY: refuses while the endpoint is online / daemon-hosted (deleting records out from under a live host risks the daemon re-creating or re-hosting mid-purge); `--force` STOPS it first (endpoint stop → wait for the daemon reconcile to un-host + reap the Psyche) THEN purges. Confirms interactively unless `--yes` (the CI path). Refuses purging the CALLER's OWN running id. All LOCAL — purge reaches only THIS node's records; a remote endpoint's records can't be touched, and its subnet-registry rows decay via the epoch-lease eviction (REQ-HAZARD-REGISTRY-DECAY). Removes: (1) the perch dir TREE recursively — owlery/<id>/ incl every nested {id}-psyche / {id}-w* / shells child (info.json, ready marker, sessions.log ledger, spool.db, inbox, .idle/.more-done sentinels, auth token); (2) the registry address (registry::unregister_address); (3) the context store — ContextStore::remove_endpoint(id): the a-<id> branch+worktree + the <id>/ rows from every p-<project> branch (the same fn `fork --delete-source` already uses); (4) node-local trust rows keyed on the id — access.json + visibility.json. Reuse-heavy: it is `fork --delete-source` generalized (recursive perch remove + unregister + remove_endpoint) + the trust-record cleanup; `endpoint rename` already enumerates the same record set + uses the same offline-only gate. (v0.12.0)
2026-09-06T13:09:01.8412048Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8412076Z 
2026-09-06T13:09:01.8412175Z ### REQ-READY-AGENT-RESUME
2026-09-06T13:09:01.8415554Z - Title: An offline ReadyAgent shows in `spt endpoint run`'s picker Resume-from-history and resumes correctly — closing the gap that today only LiveAgents do. ROOT: a harness-hosted ready bind (ReadyAgent::start_homed, ready.rs) writes info.json DIRECTLY and never appends the session ledger (unlike the shared establish_perch:250 live path), so a ready agent — though it has a session_id — produces ZERO ledger rows → the picker's offline+local Resume-from-history (which gates on ledger rows) never offers it. FIX (1): ledger the ready bind (ReadyAgent::start_homed → sessions::append Boot, mirroring establish_perch). FIX (2): `spt endpoint resume <id>` honors the adapter MANIFEST's endpoint TYPE — a ReadyAgent manifest (no [session.psyche_init]) resumes as a ready endpoint (poll listener, NO psyche-host); a LiveAgent (with psyche_init) as live. NO new bringup mode + NO picker changes (operator 2026-06-18): `spt endpoint run` is the spt-hosted ENDPOINT bringup for BOTH types, the type IS the adapter-manifest's concern (psyche-host already keys on psyche_init presence) — so (2) likely already holds; VERIFY at code, build only the residual. (v0.12.0)
2026-09-06T13:09:01.8415677Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8415706Z 
2026-09-06T13:09:01.8415811Z ### REQ-PICKER-ADAPTER-DESCRIPTION
2026-09-06T13:09:01.8417205Z - Title: The Create-new adapter-CHOICE screen of `spt endpoint run`'s picker shows a right-hand Description panel (like the Pick-existing endpoint picker's two-pane) surfacing per-adapter detail: install date, last-updated, adapter TYPE / the endpoint types it hosts, and the adapter description — so the user can see WHAT each adapter is before choosing it (today the selector lists bare names). DEFERRED fast-follow to v0.12.0 (operator 2026-06-18). (post-v0.12.0)
2026-09-06T13:09:01.8417299Z - Required stages: 
2026-09-06T13:09:01.8417328Z 
2026-09-06T13:09:01.8417434Z ### REQ-HAZARD-VIEWER-ISOLATION
2026-09-06T13:09:01.8419724Z - Title: A slow / dead / hostile VIEWER must NEVER stall the controller, the PTY child, or the session drain thread. The broker drain fans output to the controller on the authoritative blocking bounded path (advances delivered_through) but to each viewer via a bounded per-viewer channel with a dedicated writer thread; the drain `try_send`s under the log lock and a viewer whose bounded queue OVERFLOWS (can't keep up) is EVICTED (queue dropped, writer thread ends, removed from the viewers map) — the drain thread NEVER touches a viewer socket, so no viewer write can backpressure or block it. A soft viewer cap bounds the thread count. Viewer eviction never perturbs the controller stream, the delivered_through cursor, or the child.
2026-09-06T13:09:01.8419880Z - Required stages: unit, int
2026-09-06T13:09:01.8419909Z 
2026-09-06T13:09:01.8420005Z ### REQ-INSTALL-1
2026-09-06T13:09:01.8421515Z - Title: Two install paths (harness-bootstrapped calls into standalone); OS-service registration deferred. HISTORY: originally 'signed one-line script' — the hosted one-liner retired as the PUBLIC install surface at THE-FORKENING W1/W2 (ADR-0036; the canonical bootstrap is gh + the spt install verb, REQ-INSTALL-BOOTSTRAP-VERB); installer/ scripts remain in-repo as the hermetic oneliner_e2e fixture + air-gap/mirror fallback, which is what this REQ's evidence now attests (doyle-ratified 2026-07-14).
2026-09-06T13:09:01.8421678Z - Required stages: doc, impl, int
2026-09-06T13:09:01.8421707Z 
2026-09-06T13:09:01.8421801Z ### REQ-INSTALL-2
2026-09-06T13:09:01.8421920Z - Title: Marketplace-repackaging-friendly install
2026-09-06T13:09:01.8422016Z - Required stages: doc
2026-09-06T13:09:01.8422045Z 
2026-09-06T13:09:01.8422130Z ### REQ-INSTALL-3
2026-09-06T13:09:01.8422260Z - Title: Idempotent + interactive-optional first run
2026-09-06T13:09:01.8422355Z - Required stages: impl, int
2026-09-06T13:09:01.8422384Z 
2026-09-06T13:09:01.8422468Z ### REQ-INSTALL-4
2026-09-06T13:09:01.8423064Z - Title: Adapter registration lifecycle: spt adapter add (--github, manifest-first, install-is-first-update) + soft-deregister remove + optional manifest uninstall template; node-local registered-adapter set self-update ripples over
2026-09-06T13:09:01.8423166Z - Required stages: impl, unit
2026-09-06T13:09:01.8423195Z 
2026-09-06T13:09:01.8423280Z ### REQ-MIGRATE-1
2026-09-06T13:09:01.8423442Z - Title: Auto-detect and migrate a legacy claude_skill_owl install
2026-09-06T13:09:01.8423538Z - Required stages: 
2026-09-06T13:09:01.8423562Z 
2026-09-06T13:09:01.8423655Z ### REQ-INFRA-1
2026-09-06T13:09:01.8423817Z - Title: GitHub issue tracking for v1; tangled.org as migration target
2026-09-06T13:09:01.8423907Z - Required stages: 
2026-09-06T13:09:01.8423935Z 
2026-09-06T13:09:01.8424020Z ### REQ-INSTALL-5
2026-09-06T13:09:01.8425550Z - Title: Non-interactive install path: the install path doubles as every adapter's pack-in on-demand install (no second mechanism); sha256-verified fetch; user-PATH registration. HISTORY: 'the canonical one-liner' — since THE-FORKENING (ADR-0036) the canonical path is gh + the spt install verb (itself non-interactive, REQ-INSTALL-BOOTSTRAP-VERB); the scripts this REQ's evidence attests remain as the hermetic CI fixture + air-gap fallback, still non-interactive by construction (doyle-ratified 2026-07-14).
2026-09-06T13:09:01.8425655Z - Required stages: impl, int
2026-09-06T13:09:01.8425684Z 
2026-09-06T13:09:01.8425766Z ### REQ-INSTALL-9
2026-09-06T13:09:01.8426977Z - Title: Adapter add from a GitHub release archive: `spt adapter add --release <user/repo> [--tag <tag>] [--asset <name>]` fetches a `.spt` tar asset over HTTPS+GitHub trust, extracts it to the durable adapters/_github home, and registers the root — ships built binaries source-free and versioned (the distribution path for an adapter whose dev repo is a monorepo subdir, where --github root-only clone does not fit)
2026-09-06T13:09:01.8427083Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8427112Z 
2026-09-06T13:09:01.8427202Z ### REQ-INSTALL-10
2026-09-06T13:09:01.8428585Z - Title: Windows at-logon autostart runs the daemon in the background with no persistent window: the scheduled task launches `spt daemon start` (which spawn_detaches a console-less DETACHED_PROCESS daemon and exits) rather than the foreground `spt daemon run` — Task Scheduler's interactive ONLOGON launch of a long-lived console process otherwise leaves a visible console window for the daemon's whole lifetime (v0.7.4)
2026-09-06T13:09:01.8428723Z - Required stages: impl, unit
2026-09-06T13:09:01.8428751Z 
2026-09-06T13:09:01.8428838Z ### REQ-INSTALL-11
2026-09-06T13:09:01.8430703Z - Title: Adapter command templates resolve their program against the adapter's install dir BEFORE PATH: a `.spt`-shipped binary (dropped to adapters/_github/<safe>/ by --release/--github acquisition, or kept in the source_dir under copy-mode where only manifest+strings/ are copied to adapters/<name>) runs without any PATH placement — a bare-name template token (e.g. `claude-spt-digest ...`) is rewritten to <install_dir>/<program>(.exe on Windows) when that file exists, else left bare for the PATH fallback. Makes a `.spt` self-contained (closes the --release bundled-binary gap perri confirmed) (v0.7.4)
2026-09-06T13:09:01.8430813Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8430841Z 
2026-09-06T13:09:01.8430923Z ### REQ-INSTALL-12
2026-09-06T13:09:01.8433502Z - Title: Durable active-profile pointer for bind-time profile selection (ADR-0021): adapters/active-profiles.toml at the registry ROOT (sibling to the per-adapter <name>/ dirs, so adapter add/update/remove — which only rewrite a <name>/ subdir — can never clobber it), a flat host_binary → "adapter[:profile]" map. Read at bind as the PRIMARY profile selector; unset → the registered_at_ms fallback (REQ-START-5). Written ONLY by `spt adapter use <adapter>[:profile]` (resolves the adapter's host_binaries → sets each binary→adapter[:profile]); `spt adapter use --clear <adapter|binary>` drops. NEVER auto-written by install/update/adapter add (that is precisely what would let an update silently flip the active profile). A stale pointer (uninstalled adapter / deleted profile) self-heals: ignored, fall back, warn once. Pruned on adapter remove. Atomic write (spt_store atomic). (v0.9.0)
2026-09-06T13:09:01.8433677Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8433706Z 
2026-09-06T13:09:01.8433788Z ### REQ-INSTALL-13
2026-09-06T13:09:01.8436559Z - Title: Adapter add is non-destructive & idempotent-safe (F-018): `spt adapter add --github|--release` REFUSES when the target `_github/<safe>` home already backs an ACTIVE registered record — emitting an actionable code (ADAPTER_ADD_ALREADY_REGISTERED) that routes to `spt adapter update <name>` (refresh in place) or `spt adapter remove <name>` then re-add (replace) — instead of clobbering the live install (the perri footgun: `add --github` over a `--release` pointer git-cloned a source tree over the extracted built binaries → registered pointer dangled → cryptic `os error 2`). And when it DOES (re)populate the home it STAGES-THEN-SWAPS (clone/extract to a sibling staging dir, swap into place only on success) so a failed fetch/clone never strands the previously-extracted manifest+binaries as a dangling pointer (the os-2 / DeferredManifest class). Mirrors the safe stage-then-swap `adapter update` already uses (REQ-UPD-9, apply_release_crc_swap). (v0.14.1)
2026-09-06T13:09:01.8436726Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8436758Z 
2026-09-06T13:09:01.8436845Z ### REQ-REL-1
2026-09-06T13:09:01.8438363Z - Title: HISTORICAL (superseded by ADR-0036 / REQ-RELEASE-CHANNEL-PRIVATE + REQ-DOCS-LOCAL-SERVER): spt-releases publish-target repo — README public face, licensing split, Pages docs at the ADR-0014 canonical URL. The Pages pipeline retired at THE-FORKENING (docs-publish.yml deleted W2, xtask site() deleted W3); the licensing-split artifacts (releases-repo/LICENSE-*) remain in-repo and the repo-topology story lives in CONTEXT §Project infrastructure. doc stage retained as the historical record on ADR-0014.
2026-09-06T13:09:01.8438559Z - Required stages: doc
2026-09-06T13:09:01.8438583Z 
2026-09-06T13:09:01.8438668Z ### REQ-REL-2
2026-09-06T13:09:01.8439208Z - Title: Release asset set consumable by the self-updater: platform binaries, SHA256SUMS, SignedRelease metadata, manifest schema, mock-adapter zip; tag-triggered cross-repo pipeline
2026-09-06T13:09:01.8439307Z - Required stages: impl, int
2026-09-06T13:09:01.8439340Z 
2026-09-06T13:09:01.8439422Z ### REQ-REL-3
2026-09-06T13:09:01.8439840Z - Title: Two-key release-signing trust anchor: primary + offline never-used recovery, both pubkeys embedded in the binary's trusted set, manual local signing (ADR-0015)
2026-09-06T13:09:01.8439934Z - Required stages: impl, unit
2026-09-06T13:09:01.8439962Z 
2026-09-06T13:09:01.8440052Z ### REQ-DOCS-1
2026-09-06T13:09:01.8440234Z - Title: Dual-audience docs (human + AI dev-agent), markdown once / two depths
2026-09-06T13:09:01.8440324Z - Required stages: doc, impl
2026-09-06T13:09:01.8440352Z 
2026-09-06T13:09:01.8440443Z ### REQ-DOCS-2
2026-09-06T13:09:01.8440591Z - Title: Sub-10-minute runnable killer quickstart per audience
2026-09-06T13:09:01.8440686Z - Required stages: doc, int
2026-09-06T13:09:01.8440714Z 
2026-09-06T13:09:01.8440797Z ### REQ-DOCS-3
2026-09-06T13:09:01.8440968Z - Title: Diátaxis structure; one canonical way to do X
2026-09-06T13:09:01.8441063Z - Required stages: doc
2026-09-06T13:09:01.8441097Z 
2026-09-06T13:09:01.8441230Z ### REQ-DOCS-4
2026-09-06T13:09:01.8441411Z - Title: Agent-consumable layer (llms.txt, manifest schema, MCP, CLI help)
2026-09-06T13:09:01.8441507Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8441535Z 
2026-09-06T13:09:01.8441621Z ### REQ-DOCS-5
2026-09-06T13:09:01.8441788Z - Title: Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked
2026-09-06T13:09:01.8441892Z - Required stages: impl, int
2026-09-06T13:09:01.8441921Z 
2026-09-06T13:09:01.8442022Z ### REQ-HAZARD-GRACE-BEFORE-SIGNOFF
2026-09-06T13:09:01.8442193Z - Title: Grace-period wait completes before composing INIT_SIGNOFF (1.1)
2026-09-06T13:09:01.8442298Z - Required stages: impl, unit
2026-09-06T13:09:01.8442327Z 
2026-09-06T13:09:01.8442423Z ### REQ-HAZARD-INFO-JSON-TORN-READ
2026-09-06T13:09:01.8442564Z - Title: State-file reads tolerate concurrent writes (1.2)
2026-09-06T13:09:01.8442659Z - Required stages: impl, unit
2026-09-06T13:09:01.8442688Z 
2026-09-06T13:09:01.8442784Z ### REQ-HAZARD-STALE-INDEX-LOCK
2026-09-06T13:09:01.8442907Z - Title: Sweep stale lockfiles on daemon boot (1.3)
2026-09-06T13:09:01.8442997Z - Required stages: impl, unit
2026-09-06T13:09:01.8443027Z 
2026-09-06T13:09:01.8443127Z ### REQ-HAZARD-DEFERRED-DRAIN
2026-09-06T13:09:01.8443288Z - Title: Deferred spool rows excluded from the event-stream drain (1.4)
2026-09-06T13:09:01.8443380Z - Required stages: impl, unit
2026-09-06T13:09:01.8443409Z 
2026-09-06T13:09:01.8443499Z ### REQ-HAZARD-WORKER-PATH
2026-09-06T13:09:01.8443656Z - Title: Single source of truth for Worker/Psyche perch location (1.5)
2026-09-06T13:09:01.8443752Z - Required stages: impl, unit
2026-09-06T13:09:01.8443780Z 
2026-09-06T13:09:01.8443884Z ### REQ-HAZARD-PARENT-PID-PREFER
2026-09-06T13:09:01.8444052Z - Title: Prefer stable parent PID / broker handle over ephemeral PID (2.1)
2026-09-06T13:09:01.8444142Z - Required stages: 
2026-09-06T13:09:01.8444176Z 
2026-09-06T13:09:01.8444271Z ### REQ-HAZARD-STDIN-SESSION-ID
2026-09-06T13:09:01.8444447Z - Title: Stdin session_id precedence over env (2.2)
2026-09-06T13:09:01.8444533Z - Required stages: 
2026-09-06T13:09:01.8444562Z 
2026-09-06T13:09:01.8444668Z ### REQ-HAZARD-HANDOFF-ARGV-COMPAT
2026-09-06T13:09:01.8444813Z - Title: Broker/brain IPC + handoff argv version-tolerant (2.3)
2026-09-06T13:09:01.8444913Z - Required stages: impl, unit
2026-09-06T13:09:01.8444942Z 
2026-09-06T13:09:01.8445037Z ### REQ-HAZARD-GEN-START-NOW
2026-09-06T13:09:01.8445164Z - Title: gen_start = now() on cold-start and handoff (2.4)
2026-09-06T13:09:01.8445265Z - Required stages: impl, int
2026-09-06T13:09:01.8445293Z 
2026-09-06T13:09:01.8445478Z ### REQ-HAZARD-EPHEMERAL-CLEANUP
2026-09-06T13:09:01.8445627Z - Title: Ephemeral perch cleanup on every ring exit path (3.1)
2026-09-06T13:09:01.8445718Z - Required stages: impl, unit
2026-09-06T13:09:01.8445746Z 
2026-09-06T13:09:01.8445850Z ### REQ-HAZARD-RING-PERCH-ADOPTION
2026-09-06T13:09:01.8456072Z - Title: A ring NEVER adopts — and therefore never deletes — a perch directory it did not create in THIS invocation (3.4). The twin of REQ-HAZARD-EPHEMERAL-CLEANUP, which guarded the LEAK direction only: cleanup runs on every exit path, so the moment `ring` mistakes somebody's REAL perch for its own ephemeral one, that same guaranteed cleanup becomes a guaranteed DELETION — ready marker, info.json, spool.db and the dir itself, by silent CLI-side fs ops with no daemon log (field 2026-07-27: emphasys lost a live endpoint and its spooled mail this way). DELETION IS ONLY THE DURABILITY HALF: while the ring holds the adopted perch, its reply-wait `drain_one_at` CONSUMES whatever is in the victim's spool and RENDERS it to the ringer as the reply. When ringer and victim are the SAME agent (the field case) that is at worst loss — anything drained goes to its intended recipient. When a THIRD party rings a live agent it is CROSS-AGENT MAIL DISCLOSURE: the victim's inbox is drained into the ringer's output, with no trace on either side once the dir is removed, and on a subnet spanning machines that is a CONFIDENTIALITY failure, not merely a durability one. Proven by the red-first artifact — with the guard disabled the survival unit returns `Replied{from:"someone", body:"mail that must survive"}` instead of a refusal. (Field-bounded by emphasys 2026-07-27: his own 66-second ring window returned TIMEOUT with no `Replied`, so nothing was drained in THAT incident — the disclosure shape was available and did not fire.) SELF-CAMOUFLAGING, which is why it went unattributed: the damage — perch gone, roster row ghosted — MIMICS the stale-liveness condition people reach for `ring` to diagnose, so the field caller was probing a ghost-roster symptom and the probe manufactured a fresh one; debuggers of stale liveness are disproportionately its victims and read the wreckage as more of the symptom they were chasing. The misjudgement source is READY-MARKER-FIRST liveness: an existing perch whose ready marker is momentarily down (busy turn, soft session-end, stale re-bind — and on hosted OMP endpoints marker-down is the NORMAL steady state during work, since the extension owns the listener and publishes busy/idle through `api state`) reads 'caller is perchless', so marker-first misjudges HEALTHY endpoints routinely, not rarely. INVARIANT: before creating an ephemeral reply perch, probe the perch DIRECTORY, not the marker — a dir carrying a record or a spool is OCCUPIED and the ring refuses to adopt it; an UNREADABLE record (corrupt/truncated info.json) or an unreadable dir counts as OCCUPIED, never as residue, because deletion is irreversible and refusal is recoverable, so every ambiguity resolves toward refusal; a pre-existing EMPTY dir is refused too (it is not provably ring's own residue — `endpoint run` mid-create owns an empty perch dir for a window, and deleting it is the same race wearing a different hat). Refusal is never silent and never blocks: the message is DELIVERED, the call declines to block-wait (the reply lands on the caller's own listener), and the caller gets a distinct loud report — RING_PERCH_EXISTS (record/spool) or RING_STALE_DIR (empty, naming the path and the manual remedy), so the permanent-refusal failure mode is solved by LOUDNESS rather than by a self-heal that would re-introduce the deletion race. Structural, not advisory: the leaf dir is created with `create_dir` (fails AlreadyExists), so even a perch that appears between the probe and the create cannot be adopted, and the occupied-refusal path never calls cleanup.
2026-09-06T13:09:01.8456316Z - Required stages: impl, unit
2026-09-06T13:09:01.8456344Z 
2026-09-06T13:09:01.8456444Z ### REQ-PERCH-GC-RESIDUE-PREDICATE
2026-09-06T13:09:01.8464171Z - Title: A perch directory is RESIDUE only on a POSITIVE record-absence predicate, and the store that answers it is the owlery tree itself (3.4 extension; releases#109, doyle-ruled 2026-08-04). There is NO independent endpoint-existence store on a node: `spt::roster::enumerate`, the daemon's `projwriter::enumerate_perches` and `perch::list_self_perch_ids_checked` all derive the endpoint set from `read_dir(owlery)`, and a dir with no `info.json` is not a perch — so THE PERCH DIRECTORY PLUS ITS PARSEABLE RECORD IS THE ENDPOINT RECORD. The local registry is NOT that store and must never be asked: KNOWN-HAZARDS 4.3 has `clean_stale_entries` DELETE dead-pid rows and `unregister_address` fire on an ordinary stop, so registry-absence is the normal steady state of every OFFLINE endpoint (measured HFENDULEAM 2026-08-04: 6 of 14). PREDICATE: residue iff no `info.json` is present on ANY read attempt. Positive, present-tense, one read, and NEVER an age/mtime heuristic — a dormant endpoint, a suspended session and a long-idle live agent are indistinguishable from 'old' by mtime. Absent is the ONLY answer that authorizes deletion, so the read keeps NotFound distinct from every other I/O error: unreadable ⇒ occupied, corrupt (present, unparseable) ⇒ occupied (a destroyed record belongs to an endpoint that exists). REAP POPULATION IS NARROWER THAN RESIDUE: recordless AND spool-less AND non-empty AND no record-write in flight (`.info.lock` held ⇒ a bringup mid-create) AND every nested/shells child independently reapable (deepest-first; a recordless parent holding ANY non-reaped child is itself refused). A REFUSED DIRECTORY SHIELDS ITS WHOLE SUBTREE (doyle, ruled 2026-08-04): a descendant that would qualify on its own is reported SHIELDED-BY-REFUSED-PARENT and left alone, because refusing a directory must mean not modifying its CONTENTS either — carving a child out destroys part of what the refusal report just asked the operator to look at, and can move the parent's own classification on the next pass, and a sweep whose refusals are unstable across its own runs is untrustworthy. A recordless dir carrying a SPOOL is refused permanently — undeliverable-by-construction settles deliverability, not VALUE, and that spool is the only surviving copy of what was queued. An EMPTY dir is refused permanently — the mid-create race 3.4 protects exists here identically and no positive discriminator for 'not mid-create' exists for an empty dir. A reparse point anywhere in the subtree is refused. Store unavailable (`read_dir` on the root fails) ⇒ DO NOTHING and say so, reported distinctly from an empty owlery.
2026-09-06T13:09:01.8464375Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8464403Z 
2026-09-06T13:09:01.8464505Z ### REQ-PERCH-GC-REFUSAL-REPORT
2026-09-06T13:09:01.8468559Z - Title: The perch GC is REPORT-ONLY BY DEFAULT and every refusal is LOUD, distinct and per-path (releases#109, doyle-ruled 2026-08-04 — 3.4's own doctrine transplanted: loudness, not a self-heal, is what keeps a PERMANENT refusal from being a silent one). Bare `spt endpoint gc` classifies every perch directory and deletes NOTHING; `--reap` removes exactly the ruled population. There is deliberately NO `--dry-run`: when the default deletes nothing, a dry-run flag is a synonym that implies the default is dangerous. The classification pass completes BEFORE any removal, so the census prints IDENTICALLY with and without `--reap` (same subjects, same tokens, same order) and the two runs compare line for line — a bare run is a faithful preview. Each refused row names its own token (RESIDUE-WITH-SPOOL, RESIDUE-EMPTY, OCCUPIED-RECORD, OCCUPIED-CORRUPT-RECORD, OCCUPIED-UNREADABLE-RECORD, OCCUPIED-UNREADABLE-DIR, OCCUPIED-REPARSE-POINT, OCCUPIED-WRITE-IN-FLIGHT, OCCUPIED-CHILD-HELD) rather than one lumped 'skipped', and each PERMANENTLY-refused residue row carries the manual remedy that is the operator's only remaining handle. A removal that fails is reported by path with its error and exits non-zero; an unreadable owlery root exits non-zero having classified and removed nothing. The report is the evidence layer the daemon-fired sweep (releases#158) must earn its automation from.
2026-09-06T13:09:01.8468834Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8468863Z 
2026-09-06T13:09:01.8469053Z ### REQ-HAZARD-PERCH-GC-LIVENESS-INVERSION
2026-09-06T13:09:01.8472310Z - Title: A perch GC must NEVER key on `is_perch_alive` — on the residue population that predicate is INVERTED (3.5; measured HFENDULEAM 2026-08-04). `spt_store::liveness::is_perch_alive` returns TRUE for a directory with no `info.json` (`RawRead::Absent ⇒ true`, interim parity: an absent record means a listener between polls), and FALSE for a daemon-hosted endpoint whose status is `offline`. So on a real box every one of the 24 recordless residue directories read ALIVE and all 6 offline REAL endpoints read DEAD: a sweep keyed on `!is_perch_alive` does not merely under-perform, it authorizes EXACTLY THE INVERSE SET — it would have spared every stray probe dir and deleted six resting agents' records. The predicate is correct for its own job (fail toward alive is the safe direction for DELIVERY) and wrong for this one, which is why the inversion is a hazard rather than a bug. INVARIANT: the GC classifies on record PRESENCE only and never calls a liveness resolver; the unit pinning this asserts BOTH arms with the shipped resolver as the witness (residue reads alive and is still reaped; an offline endpoint reads dead and is still kept).
2026-09-06T13:09:01.8472487Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8472520Z 
2026-09-06T13:09:01.8472625Z ### REQ-HAZARD-STALE-SIGNOFF-SENTINEL
2026-09-06T13:09:01.8472773Z - Title: Stale signoff sentinel does not kill a fresh start (3.2)
2026-09-06T13:09:01.8472863Z - Required stages: impl, unit
2026-09-06T13:09:01.8472891Z 
2026-09-06T13:09:01.8472995Z ### REQ-HAZARD-ECHO-BEFORE-SIGNOFF
2026-09-06T13:09:01.8473160Z - Title: Echo-commune fires before INIT_SIGNOFF on orphan teardown (3.3)
2026-09-06T13:09:01.8473254Z - Required stages: impl, unit
2026-09-06T13:09:01.8473282Z 
2026-09-06T13:09:01.8473387Z ### REQ-HAZARD-ENVELOPE-DECODE-ORDER
2026-09-06T13:09:01.8473526Z - Title: Envelope decode order, ampersand decoded last (4.1)
2026-09-06T13:09:01.8473625Z - Required stages: impl, unit, doc
2026-09-06T13:09:01.8473654Z 
2026-09-06T13:09:01.8473750Z ### REQ-HAZARD-ENVELOPE-CR-LINESAFE
2026-09-06T13:09:01.8475484Z - Title: Envelope CR-linesafety (4.1): the line-framed EVENT codec must neutralize raw carriage returns — `event_body_escape` folds CRLF/lone-CR to the codec's representable linebreak (`\n`→`<br>`) BEFORE framing, so a body carrying `\r` (Windows `echo`/CRLF text crossing nodes) cannot survive into the single-line envelope and trigger a receiver terminal CR→col0 overwrite that corrupts the frame. Robustness on unrepresentable input, NOT a wire-format change (decoder untouched, amp-last invariant held). Belt-and-suspenders: `spt send`/`ring` also trim stdin (parity with `notify`).
2026-09-06T13:09:01.8475589Z - Required stages: impl, unit, doc
2026-09-06T13:09:01.8475622Z 
2026-09-06T13:09:01.8475723Z ### REQ-HAZARD-ENVELOPE-ATTR-LINESAFE
2026-09-06T13:09:01.8476223Z - Title: Envelope ATTR-linesafety (4.1): attribute values are line-safe because the escaper MAKES them so, not "by construction" — `event_attr_escape` folds CRLF/lone-CR to `
2026-09-06T13:09:01.8476315Z ` and then encodes `
2026-09-06T13:09:01.8479163Z ` as the `&#10;` entity, so no attribute value can split the line-framed EVENT. The falsifier is the composed trust-warning (releases#170), a multi-line block that rides as a `trust-warning` ATTRIBUTE: under the old escaper one delivery rendered as SEVEN lines under the default advisory (measured at the red, 2026-08-21), six of which are not envelopes. The count varies with the operator override, which is why the invariant is stated as "no attribute value can split the frame" and not as a number. Decode is amp-last-preserving: `&#10;` decodes BEFORE `&amp;`, exactly as `<br>` does on the body side, so an attribute carrying the literal text `&#10;` (arriving as `&amp;#10;`) decodes to the literal and never to a linebreak. Corollary held by the same commit: the codec has ONE escaper and therefore ONE decoder — `parse_event_from_attr` calls `event_attr_unescape` rather than hand-inlining a second rule that would make `from=` decode differently from every other attribute.
2026-09-06T13:09:01.8479461Z - Required stages: impl, unit, doc
2026-09-06T13:09:01.8479490Z 
2026-09-06T13:09:01.8479599Z ### REQ-BUILD-UNIQUE-BIN-NAMES
2026-09-06T13:09:01.8481886Z - Title: No two workspace packages declare a bin of the same name. Same-named bins share ONE `target/<profile>/` output filename, clobber each other, and a CARGO_BIN_EXE_* lookup then resolves to whichever built last (the PR#28 Windows gate-escape). The invariant was previously carried ONLY as prose that named a single counterpart by name, and prose of that shape ages the moment the counterpart moves: a reader greps the named bin, finds nothing, and concludes the hazard is retired. ENUMERATION IS BINDING: the check reads cargo metadata target list, NEVER `[[bin]]` manifest stanzas — a stanza grep is structurally unable to see an autodiscovered src/bin/*.rs target, so it would certify an absence it cannot observe, which is how a live bin was recorded as non-existent in the register.
2026-09-06T13:09:01.8482004Z - Required stages: impl, unit
2026-09-06T13:09:01.8482032Z 
2026-09-06T13:09:01.8482138Z ### REQ-DOCS-NO-INTERIOR-SPACE-RUNS
2026-09-06T13:09:01.8488387Z - Title: Operator-facing text carries no INTERIOR run of 3+ spaces, judged on RENDERED bytes rather than source text. The defect is a Rust line-continuation backslash that a generator or a careless edit ATE: the literal still compiles, still passes every suite, and only the rendered message is wrong, so no gate we run can see it and only a diff read ever has. The check must RENDER, because a HEALTHY continuation puts a real run of spaces in the file on the next line every time (releases#74 own instance carries thirteen and renders with one) — a source-text scan therefore flags every correctly-continued literal in the crate, and a check with that false-positive rate is one somebody switches off. SCOPE (doyle, re-ruled on the #208 evidence): IN = CLI help and CLI stdout/stderr AND daemon-emitted operator diagnostics, one surface wider than the original brief because the five literals that re-opened the class lived in daemon diagnostics; OUT = test assertion messages, read by someone already reading a trace. A deliberate run stays possible via a per-site greppable `spacerun-ok: <token>` marker carrying a REASON TOKEN, so an exception is a decision written down rather than a hole the gate cannot see; per-site is ruled, because an idiom-aware exemption would have to DETECT alignment, alignment is fuzzy, and any rule recognising `this looks aligned` eventually exempts an eaten continuation that happens to look aligned — a silent false negative in the one direction this check exists to prevent. THIS REQUIREMENT CLAIMS INTERIOR RUNS, NOT THE CLASS (doyle, 2026-08-22). A continuation dies two ways and only one is in reach: if the backslash is deleted AND the lines are joined, the indentation lands mid-sentence and is refused — the JOIN variant, which is measurably what 3416ffd3 repaired (pre-fix run = 18 spaces, those bytes now a cell); if the backslash alone is deleted, the literal keeps its newline and the indentation arrives as LEADING whitespace, which is NOT refused, deliberately, because a help block legitimately indents every line. The limit is named in the module doc as well as here so that greping this id cannot be read as the class being closed.
2026-09-06T13:09:01.8488578Z - Required stages: impl, unit
2026-09-06T13:09:01.8488602Z 
2026-09-06T13:09:01.8488711Z ### REQ-HAZARD-ENVELOPE-PARSER-SAFE
2026-09-06T13:09:01.8488873Z - Title: Two-slice envelope parser is panic-free and tolerant (4.2)
2026-09-06T13:09:01.8489040Z - Required stages: impl, unit
2026-09-06T13:09:01.8489069Z 
2026-09-06T13:09:01.8489165Z ### REQ-HAZARD-EVENTPART-REASSEMBLY
2026-09-06T13:09:01.8489364Z - Title: EVENT-PART split/reassembly is byte-exact; orphan parts dropped silently
2026-09-06T13:09:01.8489564Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8489598Z 
2026-09-06T13:09:01.8489689Z ### REQ-HAZARD-ID-CHARSET
2026-09-06T13:09:01.8489922Z - Title: Addressable-id charset reserves :/@ delimiters; validated at every creation seam (4.6)
2026-09-06T13:09:01.8490011Z - Required stages: impl, unit
2026-09-06T13:09:01.8490045Z 
2026-09-06T13:09:01.8490149Z ### REQ-HAZARD-REGISTRY-STALE-CLEAN
2026-09-06T13:09:01.8490319Z - Title: Stale registry entries degrade to fallback, never hard-fail (4.3)
2026-09-06T13:09:01.8490415Z - Required stages: impl, unit
2026-09-06T13:09:01.8490444Z 
2026-09-06T13:09:01.8490548Z ### REQ-HAZARD-REGISTRY-CONCURRENT
2026-09-06T13:09:01.8490778Z - Title: Concurrent SQLite openers (registry/spool) must not fail with 'database is locked' (4.7)
2026-09-06T13:09:01.8490873Z - Required stages: impl, unit
2026-09-06T13:09:01.8490901Z 
2026-09-06T13:09:01.8491001Z ### REQ-HAZARD-REGISTRY-DIR-CREATE
2026-09-06T13:09:01.8491365Z - Title: SQLite store opens create their parent dir themselves — a fresh-home registry op must not SQLITE_CANTOPEN (4.9)
2026-09-06T13:09:01.8491459Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8491492Z 
2026-09-06T13:09:01.8491592Z ### REQ-HAZARD-REGISTRY-EPOCH-LEASE
2026-09-06T13:09:01.8492004Z - Title: Registry merge ordered by per-node monotonic epoch, never wall-clock — a stale Active can't clobber a newer Offline (4.8, red-team #8)
2026-09-06T13:09:01.8492155Z - Required stages: impl, unit
2026-09-06T13:09:01.8492184Z 
2026-09-06T13:09:01.8492295Z ### REQ-HAZARD-DEFERRED-SURVIVE-DRAIN
2026-09-06T13:09:01.8492409Z - Title: Deferred rows survive poll drain (4.4)
2026-09-06T13:09:01.8492507Z - Required stages: impl, unit
2026-09-06T13:09:01.8492531Z 
2026-09-06T13:09:01.8492631Z ### REQ-HAZARD-INBOX-NO-DOUBLE
2026-09-06T13:09:01.8492749Z - Title: No double-delivery via legacy inbox (4.5)
2026-09-06T13:09:01.8492843Z - Required stages: impl, unit
2026-09-06T13:09:01.8492876Z 
2026-09-06T13:09:01.8492977Z ### REQ-HAZARD-WINDOWS-PID-RECYCLE
2026-09-06T13:09:01.8493120Z - Title: Windows PID-recycling false positives guarded (5.1)
2026-09-06T13:09:01.8493210Z - Required stages: impl, unit
2026-09-06T13:09:01.8493253Z 
2026-09-06T13:09:01.8493345Z ### REQ-HAZARD-EBUSY-RENAME
2026-09-06T13:09:01.8493496Z - Title: tmp-write + atomic-rename + retry on Windows EBUSY (5.2)
2026-09-06T13:09:01.8493593Z - Required stages: impl, unit
2026-09-06T13:09:01.8493621Z 
2026-09-06T13:09:01.8493731Z ### REQ-HAZARD-PERCH-RECORD-POWER-LOSS
2026-09-06T13:09:01.8494217Z - Title: Authoritative/identity records fsync data before the rename (5.13): a hard reset must not resurrect a full-length NUL-filled record — SCOPED, not a blanket fsync
2026-09-06T13:09:01.8494322Z - Required stages: impl, unit
2026-09-06T13:09:01.8494350Z 
2026-09-06T13:09:01.8494454Z ### REQ-HAZARD-ATOMIC-TMP-COLLISION
2026-09-06T13:09:01.8494876Z - Title: Concurrent atomic writers to the same target must not share a tmp name (5.15): a fixed tmp sibling makes one writer's rename consume the other's staged file (os-error-2 loser)
2026-09-06T13:09:01.8494975Z - Required stages: impl, unit
2026-09-06T13:09:01.8495003Z 
2026-09-06T13:09:01.8495098Z ### REQ-HAZARD-INFO-RMW-LOST-UPDATE
2026-09-06T13:09:01.8495484Z - Title: Concurrent info.json writers must serialize under the per-perch lock (5.16): an unlocked whole-record write racing a locked RMW is a silent lost update
2026-09-06T13:09:01.8495642Z - Required stages: impl, unit
2026-09-06T13:09:01.8495670Z 
2026-09-06T13:09:01.8495774Z ### REQ-HAZARD-CORRUPT-PERCH-COHERENCE
2026-09-06T13:09:01.8496167Z - Title: Corrupt (present-but-unparseable) info.json is NOT absent: liveness/status readers agree a destroyed record is neither alive nor Active (counter-39 #2)
2026-09-06T13:09:01.8496256Z - Required stages: impl, unit
2026-09-06T13:09:01.8496286Z 
2026-09-06T13:09:01.8496394Z ### REQ-HAZARD-SUBPROCESS-TIMEOUT
2026-09-06T13:09:01.8496529Z - Title: Every harness/git subprocess has a timeout (5.3)
2026-09-06T13:09:01.8496709Z - Required stages: impl, unit
2026-09-06T13:09:01.8496733Z 
2026-09-06T13:09:01.8496830Z ### REQ-HAZARD-UNC-PATH-STRIP
2026-09-06T13:09:01.8496967Z - Title: Strip Windows UNC prefix on serialized paths (5.4)
2026-09-06T13:09:01.8497087Z - Required stages: impl, unit
2026-09-06T13:09:01.8497125Z 
2026-09-06T13:09:01.8497229Z ### REQ-HAZARD-SINGLE-PATH-SOURCE
2026-09-06T13:09:01.8497406Z - Title: Single path/registry source of truth; no layout ambiguity (6.1)
2026-09-06T13:09:01.8497502Z - Required stages: impl, unit
2026-09-06T13:09:01.8497535Z 
2026-09-06T13:09:01.8497624Z ### REQ-HAZARD-SOFT-CLEANUP
2026-09-06T13:09:01.8497800Z - Title: Soft-cleanup preserves state, removes only the ready marker (6.2)
2026-09-06T13:09:01.8497889Z - Required stages: impl, unit
2026-09-06T13:09:01.8497917Z 
2026-09-06T13:09:01.8498021Z ### REQ-HAZARD-CASCADE-WIPE-GUARD
2026-09-06T13:09:01.8498181Z - Title: No hard-delete of a parent hosting non-empty children (6.3)
2026-09-06T13:09:01.8498274Z - Required stages: impl, unit
2026-09-06T13:09:01.8498298Z 
2026-09-06T13:09:01.8498413Z ### REQ-HAZARD-DROP-FILE-SINGLE-WRITER
2026-09-06T13:09:01.8498537Z - Title: Drop files are daemon-owned single-writer (6.4)
2026-09-06T13:09:01.8498642Z - Required stages: impl, unit
2026-09-06T13:09:01.8498670Z 
2026-09-06T13:09:01.8498771Z ### REQ-HAZARD-DIRECT-WRITE-PRECEDENCE
2026-09-06T13:09:01.8499058Z - Title: Direct-write precedence marker (with node id) guards stale overwrite (6.5)
2026-09-06T13:09:01.8499209Z - Required stages: impl, unit
2026-09-06T13:09:01.8499237Z 
2026-09-06T13:09:01.8499343Z ### REQ-HAZARD-CONFLICT-BOTH-PRESERVED
2026-09-06T13:09:01.8499872Z - Title: A surfaced concurrent context pair is durably preserved (both versions, tracked artifacts) until a strictly dominating write clears it; no reconcile failure path discards an unmerged version (6.6, ADR-0013)
2026-09-06T13:09:01.8499964Z - Required stages: impl, unit
2026-09-06T13:09:01.8499992Z 
2026-09-06T13:09:01.8500110Z ### REQ-HAZARD-DETACHED-PIPE-INHERIT
2026-09-06T13:09:01.8501247Z - Title: Windows detached long-lived children must not inherit a captured caller's pipe: every detach-spawn of an immortal child (daemon, shell binary) runs bInheritHandles=FALSE, or a caller capturing output anywhere up the process chain hangs forever on a pipe that never EOFs — std-handle flag stripping is NOT sufficient (grandparent strays still flow) (5.6)
2026-09-06T13:09:01.8501348Z - Required stages: impl, unit
2026-09-06T13:09:01.8501376Z 
2026-09-06T13:09:01.8501471Z ### REQ-HAZARD-CONPTY-DSR
2026-09-06T13:09:01.8501657Z - Title: ConPTY reader must auto-answer DSR (ESC[6n) or all child output stalls (5.5)
2026-09-06T13:09:01.8501757Z - Required stages: impl, unit
2026-09-06T13:09:01.8501780Z 
2026-09-06T13:09:01.8501886Z ### REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE
2026-09-06T13:09:01.8503727Z - Title: Native-PTY spawn must resolve a bare program name with PATHEXT precedence and run a non-PE target through its interpreter: portable-pty's own `which` takes the FIRST PATH match — an extensionless shebang shim (e.g. a node CLI `ccs` shipped beside `ccs.cmd`) — and CreateProcessW then rejects the non-PE file with os error 193 ('not a valid Win32 application'); spt-term resolves the program itself (PATHEXT order prefers .EXE over .CMD; .cmd/.bat → cmd.exe /d /c, .ps1 → powershell -NoProfile -File) so a bare harness/shell [session.self] command actually launches on Windows. Unix is a passthrough (execve honours the shebang).
2026-09-06T13:09:01.8503905Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8503933Z 
2026-09-06T13:09:01.8504042Z ### REQ-HAZARD-CHILD-CONSOLE-FLASH
2026-09-06T13:09:01.8504443Z - Title: Console-subsystem children of the console-less daemon spawn with CREATE_NO_WINDOW, or each spawn flashes a visible blank window on the user's desktop (5.8)
2026-09-06T13:09:01.8504534Z - Required stages: impl, unit
2026-09-06T13:09:01.8504563Z 
2026-09-06T13:09:01.8504667Z ### REQ-HAZARD-INSTANT-UNDERFLOW
2026-09-06T13:09:01.8505320Z - Title: Scheduling never subtracts a Duration from Instant::now() (underflow-panics on a host booted more recently than the offset); 'due now / never run' is Option<Instant>=None gated on forward duration_since only (5.9)
2026-09-06T13:09:01.8505422Z - Required stages: impl, unit
2026-09-06T13:09:01.8505450Z 
2026-09-06T13:09:01.8505554Z ### REQ-HAZARD-PUMP-IPC-DEADLINE
2026-09-06T13:09:01.8506335Z - Title: The single-threaded peer pump's brain-IPC reads are deadline-bounded (PUMP_PEER_IO_TIMEOUT, total-wait per call); a TimedOut read POISONS the client and escalates to a SUPERVISED RESTART, never a per-peer retry — a black-holed peer must never wedge the whole pump
2026-09-06T13:09:01.8506441Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8506474Z 
2026-09-06T13:09:01.8506569Z ### REQ-HAZARD-BROKER-QUIC-DEADLINE
2026-09-06T13:09:01.8509665Z - Title: The broker bounds every brain-waiting QUIC op (dial / open_stream / send_stream) so a black-holed or dead peer fails PROMPTLY with an ORDINARY error the broker REPLIES, never an unbounded await. The bound (< the brain's 30s PUMP_PEER_IO_TIMEOUT so the BROKER fires first) surfaces to the pump as a normal broker error reply → peer_outcome's non-TimedOut arm → drop conn + redial next tick, the round CONTINUES and the heartbeat keeps advancing — it must NEVER manifest as the brain's own read-deadline (the A-half poison → supervised-restart path REQ-HAZARD-PUMP-IPC-DEADLINE guards). Exactly-once is preserved: a timed-out journaled op fails INSIDE its apply_once closure so no phantom conn_id/stream_id is recorded and a fresh tick re-dials cleanly. The happy path is unchanged (a live peer completes with zero added latency; the bound only bites a non-responsive peer). This is the ROOT-cause cure for the 2.2h hfenduleam pump wedge — a dead roster peer whose QUIC path the broker awaited unbounded — recurring on hfenduleam 2026-06-16.
2026-09-06T13:09:01.8509841Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8509864Z 
2026-09-06T13:09:01.8509971Z ### REQ-HAZARD-BROKER-SEED-WIRE-SKEW
2026-09-06T13:09:01.8513505Z - Title: A daemon-state wire-format change (e.g. the v0.9.0 adapter-agnostic Seed) does NOT take effect until a DELIBERATE full broker restart: the broker serves the seed-control channel and is RESIDENT across a brain-only self-update (ADR-0004 no-terminate-during-update forbids auto-killing it), so a NEW-version CLI talking to a still-resident OLD broker fails the seed handshake — the old broker cannot deserialize the new Seed (its formerly-required `adapter` field is gone) and drops the conn without an ack, which surfaces to the CLI as a raw UnexpectedEof 'failed to fill whole buffer'. spt-core must (a) surface an ACTIONABLE diagnostic on that seed-ack EOF (name the stale-broker cause + the `spt daemon stop` fix — the broker restarts on the next api call), never the cryptic io error; and (b) document the operational rule (a deliberate broker restart is required on any daemon-state wire change — NOT automatic) + the FORWARD discipline (daemon-state/Seed schema changes stay additive + serde-default so a resident OLD broker tolerates a NEW CLI across a brain-only update; note this would NOT have rescued 0.9.0 itself, since the old broker's `adapter` was a required field). perri PREP-4 FINDING 1 (v0.9.0 CLI vs stale 0.8.x broker).
2026-09-06T13:09:01.8513628Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8513651Z 
2026-09-06T13:09:01.8513808Z ### REQ-HAZARD-SUDO-SECURE-PATH
2026-09-06T13:09:01.8514635Z - Title: Elevation guidance on Unix names the binary's ABSOLUTE path under sudo (a user-local install ~/.local/bin · ~/.cargo/bin is not on sudo's secure_path, so bare `sudo spt` dies 'command not found'); gated commands auto-elevate on an interactive TTY, else print the runnable hint (5.10)
2026-09-06T13:09:01.8514728Z - Required stages: impl, unit
2026-09-06T13:09:01.8514756Z 
2026-09-06T13:09:01.8514855Z ### REQ-HAZARD-SELF-ELEVATE
2026-09-06T13:09:01.8516967Z - Title: Self-elevation (REQ-ELEVATE-1) re-runs the EXACT original invocation with the binary's ABSOLUTE exe path — never widening privilege scope, never adding/altering args, never via a PATH-resolved bare name, never via a shell-interpolated command string (argv-array only, no `sh -c`); the elevated child drops state back to the user (composes with the 5.7 de-elevation) and NEVER re-elevates (loop-safe: decide_elevation_path returns AlreadyElevated whenever the process is already Elevated, on every OS). The user's UAC/polkit/sudo prompt is the only consent gate — we never bypass it; the print-hint floor prints the absolute-path command too. The unprivileged parent never depends on (pipes/captures) the privileged child's stdout.
2026-09-06T13:09:01.8517152Z - Required stages: unit
2026-09-06T13:09:01.8517180Z 
2026-09-06T13:09:01.8517273Z ### REQ-HAZARD-LOCAL-API-AUTH
2026-09-06T13:09:01.8517462Z - Title: Every local `api` mutation authenticated to an endpoint/session (codex #13)
2026-09-06T13:09:01.8517553Z - Required stages: impl, unit
2026-09-06T13:09:01.8517577Z 
2026-09-06T13:09:01.8517673Z ### REQ-BOUNDARY-ROTATION-CREDENTIAL
2026-09-06T13:09:01.8519137Z - Title: api boundary's rotation credential is designed, documented, and eventually anchor-proven (ADR-0032): the proof is the DEPARTED session's (prior sid / token) — --to-session-id is payload, never proof; the published surface documents the adapter prior-sid persistence pattern + loud-refusal requirement; the design-true end-state additionally accepts an OS-verified parent_pid-anchor ancestry proof making adapter sid-state optional
2026-09-06T13:09:01.8519348Z - Required stages: doc
2026-09-06T13:09:01.8519376Z 
2026-09-06T13:09:01.8519524Z ### REQ-HAZARD-RESTART-IDEMPOTENT
2026-09-06T13:09:01.8519752Z - Title: Idempotent/exactly-once delivery across brain restart at every broker boundary (codex #14)
2026-09-06T13:09:01.8519854Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8519882Z 
2026-09-06T13:09:01.8519973Z ### REQ-HAZARD-UPDATE-ROLLBACK
2026-09-06T13:09:01.8520216Z - Title: Self-update rejects version rollback; metadata expiry + adapter content signing (codex #5)
2026-09-06T13:09:01.8520311Z - Required stages: impl, unit
2026-09-06T13:09:01.8520344Z 
2026-09-06T13:09:01.8520449Z ### REQ-HAZARD-DAEMON-HOSTED-LIVENESS
2026-09-06T13:09:01.8520851Z - Title: Daemon-hosted perches (Psyche, spt-hosted Self) derive liveness from the daemon endpoint table + info.json status, never is_process_alive(info.pid) (2.5)
2026-09-06T13:09:01.8520959Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8520988Z 
2026-09-06T13:09:01.8521089Z ### REQ-HAZARD-BROKER-PROCESS-ISOLATION
2026-09-06T13:09:01.8523588Z - Title: Broker and brain are separate processes: the broker runs as its own long-lived per-machine process that survives every brain restart, so a routine (brain-only) self-update restarts the brain onto the swapped binary while every hosted endpoint (PTY child, live QUIC conn, listening socket) stays untouched at the PROCESS level. The in-process-thread broker (daemon.rs:165-170) is a regression that silently unrealizes REQ-UPD-3 — apply degrades to an in-process Brain::handoff no-op and new code does not run until an unrelated restart (KNOWN-HAZARDS 6.7). Evidence must prove process-level survival (SPIKE-01/03 productionized as int: PTY child + live QUIC survive a brain-PROCESS restart onto a swapped binary), re-pointing the regression-masked in-process int tags currently on REQ-DAEMON-2 / REQ-UPD-3 (ADR-0018).
2026-09-06T13:09:01.8523802Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8523850Z 
2026-09-06T13:09:01.8524038Z ### REQ-HAZARD-ROLLBACK-STATE-COMPAT
2026-09-06T13:09:01.8525673Z - Title: A brain must not irreversibly migrate durable state before update ready-promotion: the readiness-gated auto-rollback (ADR-0018 Q7) spawns the N-1 binary against durable state the new brain may have written, so every pre-ready write must stay N-1-readable (schema migrations gated behind ready-promotion, or written N-1-tolerant/additive). Else the first in-place schema migration silently bricks rollback (KNOWN-HAZARDS 6.8). Free now — a 2026-06-09 audit confirmed zero state-migration code exists; unmintable retroactively once a migration ships.
2026-09-06T13:09:01.8525863Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8525888Z 
2026-09-06T13:09:01.8525992Z ### REQ-HAZARD-BRAIN-RESPAWN-PATH
2026-09-06T13:09:01.8528380Z - Title: The broker respawns the brain onto the APPLIED bytes, not the renamed old binary: the candidate-binary default is the canonical exe path captured ONCE at broker start, never a per-spawn std::env::current_exe() — on Linux current_exe (readlink /proc/self/exe) is inode-tracking and follows the `apply` rename (spt -> spt.old-N), so a resident broker would respawn the brain onto OLD bytes while recording `applied` (Windows GetModuleFileName is path-at-start, so Windows was green; ADR-0018 Q3 silently assumed path-string semantics). Backstop: promotion gates on bytes — a trial promotes only if brain.ready exe_hash == the staged artifact hash for this platform, else auto-rollback + loud notif (readiness != new-bytes was the false-success that recorded applied:8 over a v0.4.0 brain on kitsubito, 2026-06-11). KNOWN-HAZARDS 6.11.
2026-09-06T13:09:01.8528505Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8528538Z 
2026-09-06T13:09:01.8528643Z ### REQ-HAZARD-PSYCHE-OUTBOUND-PROXY
2026-09-06T13:09:01.8529490Z - Title: Psyche outbound captured + sanitized: the live-Psyche turn driver captures stdout (never Stdio::null), and the daemon strips/re-stamps Psyche-supplied from=/target and constrains routing (reply→__REPLY_TO__ sender, notify→own user/subnet) (7.3)
2026-09-06T13:09:01.8529635Z - Required stages: impl, unit
2026-09-06T13:09:01.8529664Z 
2026-09-06T13:09:01.8529765Z ### REQ-HAZARD-DAEMON-SCHED-NONBLOCKING
2026-09-06T13:09:01.8530365Z - Title: Per-agent pulse/psyche/echo-commune scheduling must not serialize across agents: each agent's bounded LLM call (echo-commune summarizer, Psyche turn) runs off the shared scheduler so one slow/hung call cannot stall another agent's tick (7.4)
2026-09-06T13:09:01.8530471Z - Required stages: impl, unit
2026-09-06T13:09:01.8530499Z 
2026-09-06T13:09:01.8530608Z ### REQ-HAZARD-PAIR-TRANSCRIPT-BIND
2026-09-06T13:09:01.8531219Z - Title: Pairing transcript binds roles, both node pubkeys, subnet ID, seed epoch, TOTP time-step, and confirmation MACs — or unknown-key-share/reflection/wrong-subnet/replay pairing remain possible (ADR-0005 #12)
2026-09-06T13:09:01.8531310Z - Required stages: impl, unit
2026-09-06T13:09:01.8531344Z 
2026-09-06T13:09:01.8531448Z ### REQ-HAZARD-PAIR-SEED-ROTATION
2026-09-06T13:09:01.8531944Z - Title: Removing a node rotates the subnet seed (epoch bump) so an old node/old seed cannot rejoin; trust-store delete alone is NOT revocation because the seed is replicated to every trusted node (ADR-0005 #10)
2026-09-06T13:09:01.8532045Z - Required stages: impl, unit
2026-09-06T13:09:01.8532073Z 
2026-09-06T13:09:01.8532171Z ### REQ-HAZARD-PAIR-RATE-LIMIT
2026-09-06T13:09:01.8533005Z - Title: Subnet-global pairing rate limit: one active ceremony per subnet, shared attempt counter, exponential backoff — a public pre-trust relay + multiple seed-holders otherwise enables distributed SPAKE2 guessing (and ±1 TOTP window triples the valid-password space) (ADR-0005 #11)
2026-09-06T13:09:01.8533114Z - Required stages: impl, unit
2026-09-06T13:09:01.8533141Z 
2026-09-06T13:09:01.8533237Z ### REQ-HAZARD-WAN-ORIGIN-AUTH
2026-09-06T13:09:01.8534047Z - Title: WAN-inbound origin is transport truth, never payload: the access gate's subject (ADR-0009 origin-node whitelist) is the QUIC handshake-proven remote node id from the broker's conn/stream table — a forged origin/node field inside record bytes is inert (7.5)
2026-09-06T13:09:01.8534153Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8534181Z 
2026-09-06T13:09:01.8534273Z ### REQ-CONSENT-1
2026-09-06T13:09:01.8535135Z - Title: Consent grant store: capability x subject-agent x target-node rows, enforced at the target node, subnet-settable (replicates as security material near the trust store), revocable; gated-capability ids (remote-exec, instantiate-anywhere) reserved-but-refusing; v1 consumers are the shell spawn gates (CONTEXT Consent & security gates)
2026-09-06T13:09:01.8535342Z - Required stages: impl, unit
2026-09-06T13:09:01.8535370Z 
2026-09-06T13:09:01.8535460Z ### REQ-CONSENT-2
2026-09-06T13:09:01.8536243Z - Title: Interactive consent escalation: an ungated high-risk action routes a consent prompt to the user's most-recently-active session; allow-once / allow-always (writes a grant) / deny; pre-consent flags (can_shutdown, shell_wake_spawn_anywhere) author grants via manifest/settings (CONTEXT Consent & security gates)
2026-09-06T13:09:01.8536352Z - Required stages: impl, unit
2026-09-06T13:09:01.8536381Z 
2026-09-06T13:09:01.8536467Z ### REQ-PRES-1
2026-09-06T13:09:01.8537811Z - Title: Presence resolution: the presence datum (last_active_node, last_active_endpoint, ts) gossiped subnet-wide via the agent-interaction heartbeat (rides registry distribution, visibility-gated) + one first-class most-recently-active resolution API consumed by notif first-fire, update-consent delivery, consent escalation, and shell wake resolution (M5 scope decision 1: resolution only — the PresenceChannel endpoint stays deferred)
2026-09-06T13:09:01.8537920Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8537948Z 
2026-09-06T13:09:01.8538039Z ### REQ-SHELL-1
2026-09-06T13:09:01.8538883Z - Title: Shell hosting machinery: shell perch under the owner (type/owner/adapter_name/status/alias), broker-launched binary + api bind local-link handshake, the three channels (command durable, text+file durable + progress-queryable, sensory REST-only never spooled + dropped-unless-owner-live), owner exclusivity (CONTEXT Shell model)
2026-09-06T13:09:01.8539113Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8539141Z 
2026-09-06T13:09:01.8539250Z ### REQ-SHELL-2
2026-09-06T13:09:01.8541736Z - Title: Shell sleep/wake: link-break always closes the link, and always ends the binary it can still PROVE is ours (pre-close instruction + termination timeout, then an AUTHENTICATED force-close -- narrowed from 'always closes the binary' by BAROMETER's REQ-SHELL-KILL-AUTHENTICATED, since an unconditional kill on a recycled pid force-kills an unrelated process TREE; a kill that cannot authenticate its target is refused, named in the log, and the close continues), ephemeral teardown vs persistent offline/relink, wake_command wake-watcher (offline-only, exit-opcode supervision, exponential backoff + give-up), state-keyed wake resolution (dormant/suspended/active-elsewhere; no-reachable refuses — spawn-anywhere branch deferred), spt shutdown owner cascade + api owner-shutdown gated by can_shutdown (CONTEXT Shell sleep/wake)
2026-09-06T13:09:01.8541850Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8541879Z 
2026-09-06T13:09:01.8541985Z ### REQ-HAZARD-ELEVATED-DAEMON-SPAWN
2026-09-06T13:09:01.8543255Z - Title: The daemon always runs unelevated in the invoking user's universe, regardless of which command spawns it: an elevated spawner de-elevates (Windows: UAC linked token via CreateProcessWithTokenW; Linux: drop to SUDO_UID/SUDO_GID + the invoker's HOME) — an elevated daemon's pipes deny unelevated clients (every later spt reads not-running→spawn→bind Access-denied) and a sudo'd daemon roots the user's state universe (5.7)
2026-09-06T13:09:01.8543364Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8543392Z 
2026-09-06T13:09:01.8543550Z ### REQ-HAZARD-REGISTRY-GHOST-ROWS
2026-09-06T13:09:01.8546602Z - Title: Registry rows must decay (no immortal rows) via TWO triggers: (a) NODE-SILENCE — evict rows whose author node has not been heard (admitted inbound feed) within the eviction window, so a vanished node's rows stop poisoning bare-id resolution with phantom AcrossNodes ambiguity; AND (b) per-row OFFLINE-TTL — evict rows that have been non-routable (Offline) beyond the per-row grace even while the author node is alive, because purge/erase leaves an immortal Offline row otherwise (ghost-heal re-advertises Offline ONCE with a fresh epoch, and whole-node eviction never fires for a still-alive author) so Offline ghost rows accumulate unbounded on a remote viewer under purge/erase churn (#2-secondary). Both keyed on RECEIVER-observed state (heard-map recency / a sticky receiver-observed offline_since, NOT the gossip epoch — an epoch-keyed clock would be reset by ghost-heal's fresh-epoch re-advertise); own rows never decay; a revived/re-flapped row re-inserts (or clears its offline_since) from its durable epoch within one pump cadence (4.10)
2026-09-06T13:09:01.8546855Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8546884Z 
2026-09-06T13:09:01.8546970Z ### REQ-CLI-1
2026-09-06T13:09:01.8549533Z - Title: spt endpoint noun namespace: absorbs fork/suspend/wake/shutdown/rename/stop/digest + access (ported 1:1: allow|revoke|open|list, decision 21) + description (ex-resources blurb; bare=show, set=author); merged endpoint list [--local|--subnet <name>] grouped by subnet with SELF pinned, --detail adding the ex-resources yellow-pages blurb projection; bare spt endpoint = the list (M8 decisions 1-2, 25). SUPERSEDED (F-025 item 3): the LISTING SHAPE now lives in REQ-ENDPOINT-LIST-NODE-GROUPED (node-grouped over unique instances, not grouped-by-subnet) + REQ-ENDPOINT-LIST-REST-FILTER (suspended-hidden + --show-all); the `--local` flag was dropped by REQ-ENDPOINT-LIST-MERGE-LOCAL (the list ALWAYS merges local). This REQ owns only the endpoint noun NAMESPACE + parse surface — not the render shape.
2026-09-06T13:09:01.8549649Z - Required stages: impl, unit
2026-09-06T13:09:01.8549677Z 
2026-09-06T13:09:01.8549768Z ### REQ-CLI-2
2026-09-06T13:09:01.8550430Z - Title: spt daemon noun: run|stop|status (hidden daemon verb becomes daemon run; agent-endpoint shutdown keeps its name under endpoint); daemon status renders the pump heartbeat (last-tick recency) so a half-dead daemon is never rendered implied-healthy (M8 decisions 5, 23)
2026-09-06T13:09:01.8550534Z - Required stages: impl, unit
2026-09-06T13:09:01.8550563Z 
2026-09-06T13:09:01.8550643Z ### REQ-CLI-3
2026-09-06T13:09:01.8551328Z - Title: Agent hot path stays flat across the M8 reorg: send/ring/ready/whoami/how-to unchanged; notify moves to subnet notify while notif stays top-level; breaking renames land clean with no deprecation shims (zero external CLI consumers pre-spt-claude-code) (M8 decisions 3-4, 9)
2026-09-06T13:09:01.8551428Z - Required stages: impl, unit
2026-09-06T13:09:01.8551461Z 
2026-09-06T13:09:01.8551548Z ### REQ-CLI-4
2026-09-06T13:09:01.8553600Z - Title: User-facing CLI output is human-readable: DIRECT-USER commands (e.g. adapter update/list/use) render friendly prose instead of raw CODE:RESULT markers — "claude-spt is up to date (0.2.0)." not "ADAPTER_UPDATE_UPTODATE:claude-spt: installed 0.2.0, latest 0.2.0". Strictly bounded to the direct-user surface: the adapter-PARSED bringup tokens (SEEDED/BOUND/READY/NO_SEED on seed/listen, which adapters grep) stay machine-parseable — humanization is additive (a human line beside the marker, or a --porcelain/--quiet split), never a silent rename of a dual-contract marker. The user-facing bringup composition belongs to the adapter (perri); this REQ owns only the direct-user CLI surface. (v0.9.0)
2026-09-06T13:09:01.8553699Z - Required stages: 
2026-09-06T13:09:01.8553728Z 
2026-09-06T13:09:01.8553820Z ### REQ-SUBNET-5
2026-09-06T13:09:01.8554929Z - Title: Per-subnet serve-state: spt subnet detach <NAME> [--save] / attach <NAME> [--save] — daemon keeps running, stops/starts advertising + connecting for that subnet (peer pump + responder selective); --save persists the startup default in daemon config; the all-attached banner gains per-subnet states (M8 decision 6, --save renamed from --auto per decision 25 session)
2026-09-06T13:09:01.8555097Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8555126Z 
2026-09-06T13:09:01.8555211Z ### REQ-SUBNET-6
2026-09-06T13:09:01.8555868Z - Title: Trust lifecycle verbs, elevation-gated: spt subnet leave <NAME> (membership exit) and spt subnet prune <node> (removes a dead identity's trust + registry rows, killing its dead dials; trust mutation = security surface, REQ-PAIR-6 gate machinery) (M8 decisions 6-7)
2026-09-06T13:09:01.8556059Z - Required stages: impl, unit
2026-09-06T13:09:01.8556087Z 
2026-09-06T13:09:01.8556171Z ### REQ-SUBNET-7
2026-09-06T13:09:01.8557754Z - Title: Per-machine re-pair trust overwrite: registry rows carry a hashed stable machine identifier (OS machine id /etc/machine-id|MachineGuid, domain-separated SHA-256 before gossip, spt-minted persisted UUID fallback; additive serde-default field — old rows parse clean); a COMPLETED pairing ceremony presenting the same node label AND machine id as an existing trusted row evicts the superseded identity's trust + registry rows on the seed-holder and replicates the eviction; a gossiped claim alone never evicts trust (M8 decisions 13, 22)
2026-09-06T13:09:01.8557908Z - Required stages: impl, unit
2026-09-06T13:09:01.8557936Z 
2026-09-06T13:09:01.8558021Z ### REQ-SUBNET-8
2026-09-06T13:09:01.8559159Z - Title: Status render honesty: zero-subnet text is daemon-aware ('No subnets registered — this node is standalone.' + daemon-running-dependent blurb, never implying messaging works while the daemon is down); hint footer prints on bare spt subnet only (status drops it); a stalled pump is surfaced in subnet status, never rendered implied-healthy (M8 decisions 11-12, 23)
2026-09-06T13:09:01.8559284Z - Required stages: impl, unit
2026-09-06T13:09:01.8559318Z 
2026-09-06T13:09:01.8559402Z ### REQ-INSTALL-6
2026-09-06T13:09:01.8560539Z - Title: Linux elevation install leg: install.sh symlinks the binary into a sudo-reachable path (/usr/local/bin; graceful print-the-one-liner when unelevated) so sudo spt resolves; first sudo spt detects elevation and prompts ONCE for the default user account — thereafter any elevated daemon launch runs daemon + state under that account, never root (KH 5.7 interplay verified) (M8 decision 8)
2026-09-06T13:09:01.8560632Z - Required stages: impl, unit
2026-09-06T13:09:01.8560661Z 
2026-09-06T13:09:01.8560747Z ### REQ-INSTALL-7
2026-09-06T13:09:01.8567942Z - Title: Windows inbound reachability: the install path RECONCILES the product inbound-UDP firewall rule onto the binder it places — create when missing, REPOINT when the rule names a different image, and report which it did — and the daemon self-detects blocked inbound and renders it as the no-connection state in subnet status + the coming-online banner (covers user-scope installs that skip the elevated leg — never a silent NO_SEED_HOLDER dead-end) (M8 root cause 3). AMENDED BY REPLACEMENT (releases#173, doyle ruling 2026-08-21): the previous title said the elevated leg REGISTERS the rule, which is create-only and describes half the behaviour. A program-scoped rule admits exactly one path, so the moment the binder can move is the moment the rule can go stale — and a stale rule reads GREEN BY NAME while inbound is dead on the Public profile (KNOWN-HAZARDS 7.61; field specimen 2026-08-06, a rule naming an actions-runner debug exe on a node whose installed binder serves, still reproducing at c62904e7). REPOINT-IF-DIFFERENT IS THE LOAD-BEARING ARM and create belongs to first install: an update swaps the binary in place at the same canonical path, so the program scope does not change and the reconcile is a no-op in the common case. SCOPED TO THE PRODUCT-NAMED RULE ALWAYS — the probe queries by name and the repair deletes by name, so dev and CI rules naming spt images on the same box are never read and never touched; they can be load-bearing for runner jobs, and a delete-by-image sweep would read as tidying while eating one. Elevation unavailable degrades LOUD and NEVER FATAL: the rule is left exactly as it was and the operator is handed the exact command, because a placed binder with a stated reachability problem beats a refused installation. The durable verdict is NOT written by the installer — the inbound record is pinned to the binder pid AND image (REQ-INBOUND-VERDICT-RECORD-BINDER-PINNED), so a record authored by a short-lived installer re-derives as Unknown for every reader, which is the self-erase class releases#172 closed; the verdict stays the daemon's to write at bind. Gate: doc — the CONTEXT.md install entry stating that placement reconciles the rule and what happens unelevated; impl — the pure reconcile decision seam, its thin netsh effector, and the call at binder placement; unit — the four decision cases (missing, correct, stale path, cannot-elevate) decided with no netsh and no privilege, plus a named cell proving a reconcile never reaches a rule it was not asked about.
2026-09-06T13:09:01.8568276Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8568305Z 
2026-09-06T13:09:01.8568395Z ### REQ-INSTALL-8
2026-09-06T13:09:01.8569487Z - Title: OS-service registration (REQ-INSTALL-1's deferred third leg): Linux systemd USER service + loginctl enable-linger (linger rides the elevated install leg; daemon starts at boot pre-login, user universe per KH 5.7, systemctl --user managed); Windows scheduled task at-logon (interactive session, no stored credentials); a node is reachable after reboot without any manual spt invocation (M8 decision 17)
2026-09-06T13:09:01.8569596Z - Required stages: impl
2026-09-06T13:09:01.8569624Z 
2026-09-06T13:09:01.8569710Z ### REQ-CONV-1
2026-09-06T13:09:01.8571061Z - Title: Peer address seeding, both cold starts: durable peer-addrs.json (identity dir) maps peer pubkey → last-known dialable address; the pump's resolver consults it FIRST with id-only discovery fallback on miss or dial failure (a stale addr never strands a peer); written by the pairing ceremony (both sides, from the live connection) and by the pump on successful connect; post-join first sync and post-restart resync converge in seconds, not ~1 min (M8 decisions 14, 20)
2026-09-06T13:09:01.8571164Z - Required stages: impl, unit
2026-09-06T13:09:01.8571192Z 
2026-09-06T13:09:01.8571279Z ### REQ-CONV-2
2026-09-06T13:09:01.8572325Z - Title: Event-driven advertisement: endpoint online/offline transitions (ready-listener start/stop, rest-state transition, perch death) trigger an immediate advertise_local + peer push as a WAKE of the existing pump loop (no second advertisement path — epoch lease + visibility gates ride unchanged); the cadence stays the steady-state floor (M8 decision 15)
2026-09-06T13:09:01.8572425Z - Required stages: impl, unit
2026-09-06T13:09:01.8572454Z 
2026-09-06T13:09:01.8572544Z ### REQ-PAIR-8
2026-09-06T13:09:01.8573793Z - Title: NTP TOTP offset: the pairing ceremony queries NTP at ceremony time (both sides) and applies the derived offset to the TOTP calculation in-process only; system-clock fallback when NTP is unreachable (offline LAN pairing unaffected — NTP failure never blocks a pairing that succeeds today); never sets the OS clock; no background sync loop (M8 decision 18; field trigger: enlyzeam clock >1 min off exceeds the ±1 window)
2026-09-06T13:09:01.8573899Z - Required stages: impl, unit
2026-09-06T13:09:01.8573928Z 
2026-09-06T13:09:01.8574017Z ### REQ-DAEMON-5
2026-09-06T13:09:01.8575283Z - Title: Pump liveness: the peer pump writes a last-tick heartbeat consumed by daemon status / subnet status (decision 23 render legs in REQ-CLI-2/REQ-SUBNET-8); the daemon supervises the pump task — a panic is caught, logged loudly, and the pump restarts with capped backoff (≤5 min), so a 5.9-class death self-heals visibly instead of silently halving the daemon (M8 decision 23; field motivation: hfenduleam 2026-06-07 half-death)
2026-09-06T13:09:01.8575387Z - Required stages: impl, unit
2026-09-06T13:09:01.8575478Z 
2026-09-06T13:09:01.8575559Z ### REQ-DAEMON-6
2026-09-06T13:09:01.8577405Z - Title: Service-aware `daemon start`/`stop`: when an OS service manager has a registered spt-daemon for this user, `spt daemon start` and `spt daemon stop` drive THAT service (so stop doesn't IPC-kill a unit that auto-restart-fights for the broker socket — the kitsubito 2026-06-08 loop). `start` graduates from a `run` alias to a first-class background verb (ensure-up, idempotent, non-blocking); stop routes managed→manager, manual→IPC. Linux=systemd user unit (`systemctl --user start|stop|is-active spt-daemon`, detected by unit-file presence); Windows=no controllable manager (the logon task is boot-only), so start=detached spawn / stop=IPC.
2026-09-06T13:09:01.8577606Z - Required stages: impl, unit
2026-09-06T13:09:01.8577630Z 
2026-09-06T13:09:01.8577720Z ### REQ-DAEMON-7
2026-09-06T13:09:01.8579198Z - Title: `daemon run` is foreground-consistent on every platform: the invoking process IS the daemon, blocks until signalled, never auto-detaches or respawns into an invisible background task. The detached/de-elevated background behavior lives ONLY in `start`. Windows: an ELEVATED `daemon run` refuses with guidance (use `start`, or an unelevated shell) instead of respawning detached/de-elevated and vanishing (KH 5.7 preserved — it still never serves elevated).
2026-09-06T13:09:01.8579376Z - Required stages: impl, unit
2026-09-06T13:09:01.8579405Z 
2026-09-06T13:09:01.8579489Z ### REQ-DAEMON-8
2026-09-06T13:09:01.8580404Z - Title: Internal auto-start prefers the service: `ensure_running` (any spt command's implicit daemon start, REQ-DAEMON-3) routes through the service-aware start path — when a manager has a registered service it starts THAT, never a competing manual `spawn_detached` daemon that would fight the service for the socket.
2026-09-06T13:09:01.8580513Z - Required stages: impl, unit
2026-09-06T13:09:01.8580541Z 
2026-09-06T13:09:01.8580624Z ### REQ-DAEMON-9
2026-09-06T13:09:01.8582904Z - Title: Net-bind boot-race resilience: a daemon that comes up net-less (NetHost::start failed — e.g. the systemd unit autostarted before the network/DNS stack was ready, `Failed to create an address lookup service`) must SELF-HEAL — retry the net bring-up in the background with capped backoff and, on success, attach net to the broker + spawn the dispatcher/peer-pump (which today are gated on `net_up` at boot and so never start, leaving the node silently unreachable until a manual restart — kitsubito 2026-06-08). Status surfaces the net-less state honestly (a net-less broker renders as 'no connection', not only a pump-STALLED line with a bogus pre-boot heartbeat age). The installer's autostart unit waits for the network (`Wants=/After=network-online.target`) as belt-and-suspenders.
2026-09-06T13:09:01.8583014Z - Required stages: impl, unit
2026-09-06T13:09:01.8583042Z 
2026-09-06T13:09:01.8583142Z ### REQ-HAZARD-LIVEHOST-BOOT-RACE
2026-09-06T13:09:01.8586697Z - Title: The brain's daemon-hosted Psyche lifecycle surfaces a host-FAILURE on the live perch (harness-diagnosable) and runs net-INDEPENDENTLY. When reconcile_once→host_one→spawn_psyche fails for a state=live_agent+status=online endpoint (e.g. the adapter's psyche binary absent from its install dir, REQ-INSTALL-11), the failure MUST be written to the perch info.json as a CURRENT-STATE field (reason + ts + attempt count; overwritten each 5s retry, CLEARED on successful host) and surfaced by `spt endpoint list`/status — never left as an eprintln on the brain's invisible stderr where a harness reading only perch state is blind. status=online stays authoritative (agent reachable; only the Psyche is missing — brain-restart rehydrate legitimately has online-without-Psyche windows), so this is a SEPARATE psyche-host-health field, never a status de-stamp. Net-independence is a locked-in invariant: spawn_live_host (brainproc.rs:230) reaches the reconcile and hosts the Psyche on a net-less/unpaired/peer-pump-STALLED node, proven by a REAL detached-daemon E2E (real broker→brain-child, real api seed+listen, real install-dir psyche binary). spt-core SURFACES the failure; the adapter owns fixing its packaging.
2026-09-06T13:09:01.8586867Z - Required stages: impl, unit
2026-09-06T13:09:01.8586896Z 
2026-09-06T13:09:01.8586997Z ### REQ-HAZARD-TEMPLATE-ARGV-FILL
2026-09-06T13:09:01.8590898Z - Title: Command-template substitution fills argv ELEMENTS, not a re-tokenized string: spt-core currently `fill_template`s {key} values INTO the command STRING and THEN `tokenize`s the filled string (runtime.rs:94/122), so a multi-word {key} value whitespace-SPLITS into multiple argv tokens unless the adapter hand-quotes the placeholder, and a value containing a `"` (or `;`) injects/breaks tokenization (shell-injection-adjacent). A filled value MUST become exactly ONE argv element regardless of spaces/quotes in the value. Fix: tokenize the TEMPLATE into argv FIRST, then `fill_template` EACH token, so a `{key}` slot resolves to a single element and the value never participates in tokenization (no whitespace-split, no quote/semicolon injection); preserve the missing-key / empty-command errors and `{{`/`}}` non-interpretation. perri's F-009 (v0.8.1 dogfood, argv-capture-confirmed): a multi-word `{psyche_prompt}` = "PSYCHE REVIVAL time: epoch-ms:… incoming event: (none)" arrived as argv[6..12] (7 stray tokens), the harness runner strict-parsed `--prompt` against the 2nd word, exited 2 within ~1s → phantom hosted perch. Applies to EVERY [session.<role>] template (psyche_init, extractor, notif, …); digest survives today only because its fills ({session_id}/{source}) are single-token.
2026-09-06T13:09:01.8591155Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8591194Z 
2026-09-06T13:09:01.8591303Z ### REQ-HAZARD-LIVEHOST-NONRESIDENT
2026-09-06T13:09:01.8594585Z - Title: A daemon-hosted Psyche that spawns then EXITS IMMEDIATELY is a host failure, surfaced like a spawn failure (closes the v0.8.1 residual masking): the REQ-HAZARD-LIVEHOST-BOOT-RACE signal stamps `psyche_host_error` only when `spawn_psyche` returns Err, NOT when the detached spawn() returns Ok but the child dies within moments (e.g. a bad-argv child exiting 2 — the F-009 case). That leaves the residual 'online + no Psyche + no cause' gap: the nested `{id}-psyche` info.json is written status=online with a real-but-DEAD pid and the PARENT perch carries NO psyche_host_error (perri's F-010: tasklist showed 0 host procs across the window while info.json read online). The host MUST confirm RESIDENCY — a hosted child not alive (or whose `{id}-psyche` perch never re-registers / has a dead pid) within N seconds of spawn is treated as a host failure: stamp the parent perch `psyche_host_error{reason:"host not resident within <n>s (psyche perch missing/dead pid)"}` (and do not leave a phantom online nested perch). Closes the last masking gap the v0.8.1 fix left open. perri's F-010 (v0.8.1 dogfood). Sibling of REQ-HAZARD-LIVEHOST-BOOT-RACE.
2026-09-06T13:09:01.8594695Z - Required stages: 
2026-09-06T13:09:01.8594724Z 
2026-09-06T13:09:01.8594818Z ### REQ-HAZARD-EPOCH-RESET
2026-09-06T13:09:01.8596150Z - Title: Advertisement-epoch reset strands a node: peers' higher last-seen epoch drops the reset node's fresh advertisements as Stale until the counter outruns history. Common case (full reinstall/re-pair) is mitigated by REQ-SUBNET-7's ceremony eviction (peer-side epoch memory dies with the deleted row — acceptance-verified); the residual narrow slice (epoch file lost, identity kept) is documented, guard deferred to a field hit (4.11)
2026-09-06T13:09:01.8596245Z - Required stages: 
2026-09-06T13:09:01.8596273Z 
2026-09-06T13:09:01.8596361Z ### REQ-MESH-1
2026-09-06T13:09:01.8598545Z - Title: Membership proof (seed-proof): symmetric current-epoch seed-knowledge replaces is_trusted at EVERY inbound gate (registry apply, WAN receive, sync, notif, connection accept). MK = HKDF(seed, domain ‖ subnet_id ‖ seed_epoch); mutual channel-bound challenge-response at connect (transcript binds both handshake-proven node pubkeys, both nonces, subnet_id, seed_epoch, role); verified once per connection, cached on the broker ConnEntry, kept warm via QUIC keep-alive so re-proof is restart/partition/rotation-only. Exact-epoch match (re-seed is the sole N-1 exception). SECURITY INVARIANTS: channel-bound (no cross-connection replay), mutual, accepts a member it never paired (the mesh property).
2026-09-06T13:09:01.8598716Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8598745Z 
2026-09-06T13:09:01.8598826Z ### REQ-MESH-2
2026-09-06T13:09:01.8601596Z - Title: Member roster: node-level union-merge grow-set (per member: pubkey, label, machine_id, last-known address, last-seen — NOT the seed), the discovery directory the mesh dials by. Seeded IN FULL at pairing (seed-holder hands joiner the whole current roster, incl. offline members — folds in deferred pairing-time hostname capture + post-join address seeding); each node authors its own entry stamped with its lease_epoch, merged strictly-greater-wins (the node_label lease); exchanged only over seed-proof'd member connections; forgery-inert (a fake entry names a pubkey that still can't seed-proof). Removal needs a TOMBSTONE — a per-pubkey revoked marker that propagates, dominates the entry, gates admission (seed-proof ∧ ¬tombstoned), and prevents reinsert; cleared by a completed re-pair of that pubkey. Persists through silence (offline member keeps its entry).
2026-09-06T13:09:01.8601854Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8601882Z 
2026-09-06T13:09:01.8601967Z ### REQ-MESH-3
2026-09-06T13:09:01.8603623Z - Title: Mesh row fan-out: registry rows stay OWN-AUTHORED; the only change is the push target widens from directly-paired peers to ALL roster members (a wider DIRECT fan-out, never a third-party relay). Every row/message still arrives from its author over a handshake → KNOWN-HAZARDS 7.5 (origin = handshake node) and 4.10 (eviction lease: any future update comes from that node itself, alive) PRESERVED VERBATIM. Closes the staggered A→B→C repro: C (roster-seeded with A at pairing) initiates to A, seed-proof admits C unpaired, A learns C, both push directly.
2026-09-06T13:09:01.8603749Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8603777Z 
2026-09-06T13:09:01.8603862Z ### REQ-MESH-4
2026-09-06T13:09:01.8606906Z - Title: Revoke + timeboxed seed rotation + re-seed grace: `spt subnet revoke <node>...` (list, elevation-gated, revoke-only; COMPOSED-WITH milestone-A FF: REQ-SUBNET-REVOKE-ADMIN-GATE adds the admin-TOTP gate BEFORE any write — elevation alone no longer authorizes — and REQ-SUBNET-ADMIN-RESURFACE rides the same coalescing window for the parked ADMIN key; this REQ keeps the revoke mechanics + MEMBER-seed rotation) writes roster tombstones immediately, then schedules ONE seed rotation (re-mint seed, bump seed_epoch, push new seed CONFIDENTIALLY over member-auth'd TLS connections — never in roster/registry gossip — force-drop revokees) at the close of a coalescing window (default 1h); further revokes in the window join the same rotation (one epoch bump). `--force-rotate-seed` rotates immediately (compromised-node path). RE-SEED GRACE: a node proving the immediately-prior epoch (N-1) AND still on the roster gets a re-seed-only restricted connection (auto-heals a benign offliner); revoked/off-roster denied; ≥2 stale → re-pair.
2026-09-06T13:09:01.8607025Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8607053Z 
2026-09-06T13:09:01.8607134Z ### REQ-MESH-5
2026-09-06T13:09:01.8608523Z - Title: Hard cutover from pairwise trust: delete peers.json + the is_trusted authorization path (no migration — expendable test fleet, re-pairs fresh under the new model, user decision 2026-06-08). Warn-on-change DEMOTED from a gate to an awareness notice anchored on machine_id (not label): 'machine M, last seen as K1, now presents K2' — fires the same event as the REQ-SUBNET-7 re-pair overwrite. The TrustStore/peers.json code and its call sites are removed, not left dead.
2026-09-06T13:09:01.8608631Z - Required stages: impl, unit
2026-09-06T13:09:01.8608659Z 
2026-09-06T13:09:01.8608739Z ### REQ-MESH-6
2026-09-06T13:09:01.8610107Z - Title: Concurrent liveness probes: `spt subnet status --nodes` fans out its offline/serve-probes (REQ-SUBNET-5) CONCURRENTLY — total wall-time bounded by the single-probe ceiling (~3s), never k×ceiling. The mesh makes a node see ALL members (many possibly offline), so a serial probe loop would be offline_count×3s. (Planning verifies the current REQ-SUBNET-5 probe loop's behavior and fixes it if serial.)
2026-09-06T13:09:01.8610211Z - Required stages: impl, unit
2026-09-06T13:09:01.8610240Z 
2026-09-06T13:09:01.8610326Z ### REQ-SHELL-3
2026-09-06T13:09:01.8612123Z - Title: Drive channel (owner->shell, REST-only, never-spooled, latest-wins): the owner->shell mirror of sensory for continuous real-time control (scroll/crank/stick/avatar) — a [shell.drive] manifest vocab + EVENT_TYPE_DRIVE frame, delivered to the ONLINE binary only via a single live slot (a new frame supersedes an undelivered one — no spool, no queue, no replay on relink), dropped-with-diagnostic if the shell is offline; cross-node rides the ephemeral link (REST class), never the durable shell spool. Commands = discrete+durable; drive = continuous+ephemeral (CONTEXT:260, minted 2026-06-11 Gateway grill).
2026-09-06T13:09:01.8612399Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8612428Z 
2026-09-06T13:09:01.8612508Z ### REQ-SHELL-4
2026-09-06T13:09:01.8614256Z - Title: Shell tunnel (reliable-ordered opaque byte stream): an owner<->shell link may hold a long-lived, reliable-ordered, link-bound QUIC stream pair carrying opaque wire protocol traffic the channel taxonomy must NOT reinterpret (first consumer usbip URB) — manifest opt-in, not enveloped, not MAC-framed, not spooled; the link lifecycle governs it (a link-break closes the tunnel). Reliable-ordered ⇒ congestion surfaces as lag never loss ⇒ acceptable only on-LAN: the on-LAN posture is documented and the tunnel is NOT proven cross-WAN (CONTEXT:262, minted 2026-06-11 Gateway grill; doyle gate C2).
2026-09-06T13:09:01.8614370Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8614398Z 
2026-09-06T13:09:01.8614488Z ### REQ-CONSENT-3
2026-09-06T13:09:01.8616291Z - Title: Per-capability approval gates (class-keyed): the require_approval enum may ride INDIVIDUAL [shell.capabilities] entries — gating the dangerous ACT, not just the spawn — with an optional class_key scoping the grant qualifier finer than the capability id ((owner endpoint x device class x node); a remembered HID-class attach grant never authorizes a storage-class attach). Reuses the grant store + interactive escalation + tighten-only floor (REQ-CONSENT-1/2 plumbing). Spawn gates govern EXISTENCE; capability gates govern ACTS — an explicitly distinct invariant (CONTEXT:283, ratified 2026-06-11 Gateway grill).
2026-09-06T13:09:01.8616406Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8616435Z 
2026-09-06T13:09:01.8616516Z ### REQ-SHELL-5
2026-09-06T13:09:01.8617704Z - Title: Shell ownership is owner-type-agnostic: any non-Shell endpoint type may own/spawn/drive/command/link a shell (Gateway the named first) — control-exclusivity keys on the owner endpoint_id, NEVER on the owner's endpoint type. No ownership path (mint, launch, owner-from-link, cmd, drive, tunnel, sleep/wake, owner-shutdown) inspects the owner's type (CONTEXT:264, ratified 2026-06-11 Gateway grill).
2026-09-06T13:09:01.8617824Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8617848Z 
2026-09-06T13:09:01.8617947Z ### REQ-HAZARD-VIEWER-CLOSE-DETACH
2026-09-06T13:09:01.8627506Z - Title: A VIEW is independent from the endpoint: closing the tab/window where the endpoint was brought up must detach ONLY the `spt rc` attach pump — the daemon-hosted harness keeps running and stays re-attachable via `spt rc <id>`. ROOT (Windows, v0.12.0 real-harness defect): the daemon never breaks away from the launching terminal's Job Object. Windows Terminal / VS Code place the launched shell AND every descendant into a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; closing the tab drops the terminal's last job handle → the OS terminates every process still in that job. A child escapes only with CREATE_BREAKAWAY_FROM_JOB — used NOWHERE in the tree. Both daemon spawn paths (daemon.rs:707 detached_no_inherit = DETACHED_PROCESS|CREATE_NEW_PROCESS_GROUP|CREATE_NO_WINDOW; deelevate.rs:519 elevated = CREATE_NEW_CONSOLE|...) drop the CONSOLE but NOT job membership, so the daemon's freshly broker-spawned ConPTY harness subtree is reaped on tab-close. The ConPTY/pseudoconsole isolation itself is CORRECT (portable-pty builds the pseudoconsole in the daemon; no console signal / handle leak) — the leaking lifetime binding is the Job Object, not the console. FIX: add CREATE_BREAKAWAY_FROM_JOB to both daemon spawn paths AND pin each broker-spawned harness into a DAEMON-OWNED Job Object (mirror reap.rs/Breap) as backstop (survives even where a terminal sets SILENT_BREAKAWAY_OK=false). Unix: the daemon's own session detachment (new session, no controlling terminal) already keeps a closing terminal's SIGHUP off its children — verify, add a guard test, no code expected. FIX UPDATE (v0.12.1 L1.5, doyle re-scope operator-approved 2026-06-18): job-neutral daemon launch is now PRIMARY, breakaway DEMOTED to a fallback rung. ROOT reframed — the daemon INHERITS the terminal's Job because spawn_detached runs FROM the terminal-child CLI (DETACHED_PROCESS detaches the console, not the job); breakaway tried to claw back out but a job CAN deny it (the L1 finding). FIX: launch the cold-started daemon via a job-NEUTRAL creator so it is WmiPrvSE/Task-Scheduler-owned, OUTSIDE any terminal job from birth (why Task-Scheduler-autostarted daemons never had this bug). Launcher ladder (first-success-wins, daemon.rs spawn_detached → BOTH cold-start AND `spt daemon start`): (1) WMI Win32_Process.Create via ABSOLUTE powershell -EncodedCommand (KH 5.12 abs path; base64-UTF16LE dodges all quoting; success requires BOTH ReturnValue==0 AND a parsed ProcessId, else fall-through — never a silent launched), forwarding SPT_* env via a `cmd /c set … & start /b` wrapper because a WMI/scheduler child does NOT inherit transient shell env (verified — SPT_HOME would be lost, wrong universe); (2) schtasks one-shot (same env wrapper; best-effort fallback); (3) CREATE_BREAKAWAY_FROM_JOB (the L1 code, reordered below); (4) in-job last resort (logs DETACH_IN_JOB + tab-close caveat). detached_no_inherit (breakaway-then-in-job) is UNCHANGED for its other caller shellhost::launch_shell (a daemon-spawned shell is already job-neutral once the daemon is). The elevated deelevate path keeps its L1 breakaway for now (elevated-case WMI-reparent = FOLLOW-UP). (v0.12.1)
2026-09-06T13:09:01.8627845Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8627883Z 
2026-09-06T13:09:01.8627983Z ### REQ-HAZARD-ATTACH-WEDGE
2026-09-06T13:09:01.8633209Z - Title: A legitimately dead PTY child (real crash/kill) + an undrained operator pump must NOT wedge the broker for all other clients. ROOT (v0.12.0 real-harness defect): loopback attach output is a blocking write_all into a bounded 64KB tokio duplex (nethost.rs:1040,1090); when the operator's rc pump stops draining (tab closed) the buffer fills and write_all blocks forever (the 'loopback never hangs' assumption at nethost.rs:1103 is false), parking a worker in the 2-worker net runtime (nethost.rs:640); a couple of these saturate BOTH workers → every new attach / `endpoint run` stalls right after 'PUMP_IPC_READER: spawned' → 30s FIRST_EVENT_GRACE → 'no output / dead or wedged'; `daemon stop` cannot join the stuck workers. DISTINCT from the removed B1 path-(c) mutex deadlock. DISPOSITION = PROVE-DON'T-CHANGE (doyle GATE-PASS @e883f45, 2026-06-18): this ROOT is the SUPERSEDED v0.12.0 hypothesis — the post-L0 code ALREADY prevents the wedge, so NO fail-fast / worker-count code was added. serve_attach forwards fire-and-forget (net_stream_send op_id=None) and the broker-side send_stream is already BROKER-QUIC-DEADLINE-bounded (bounded_block_on, 10s); the loopback duplex is drained broker-INTERNALLY by the operator row's own read pump (RecvHalf::Loopback, retentive_cap==0 → evict-not-park) so a dead rc (a dropped IPC subscriber) never backs peer_w up; bounded_block_on parks the BROKER DISPATCH thread, not a net worker → no worker-pool exhaustion (full mechanism in the required_stages comment). Folds the status=online sub-check: a dead spt-hosted endpoint is marked OFFLINE within one reconcile tick on abrupt child death (broker exit-waiter reaps the session → B2 sees it absent) — PROVEN, no change. (v0.12.1)
2026-09-06T13:09:01.8633390Z - Required stages: int
2026-09-06T13:09:01.8633425Z 
2026-09-06T13:09:01.8633520Z ### REQ-PICKER-HISTORY-FRESH
2026-09-06T13:09:01.8634611Z - Title: The `spt endpoint run` picker shows project history for FRESH endpoints (operator-raised v0.12.0 real-harness finding). Symptom: a fresh endpoint shows no project history in the picker. ROOT TBD — investigate the project-history loader (v0.10.0 PICKER-2, picker/data.rs) before fixing: distinguish a real loader bug from 'fresh = no history yet' semantics. (v0.12.1)
2026-09-06T13:09:01.8634826Z - Required stages: impl, unit
2026-09-06T13:09:01.8634944Z 
2026-09-06T13:09:01.8635049Z ### REQ-PICKER-ONLINE-ACTION
2026-09-06T13:09:01.8636664Z - Title: The `spt endpoint run` picker shows the correct action for an ALREADY-ONLINE endpoint — Attach, NOT 'Start now' (operator-raised v0.12.0 real-harness finding). Symptom: the picker offers 'Start now' for endpoints that are already online. ROOT TBD — investigate the status→action mapping (v0.10.0 PICKER-1 four-state status, picker/model.rs): is it reading live/online state correctly, or rendering stale/wedged broker state (i.e. partly a symptom of the broker wedge / status=online latch)? Fix so online → Attach. (v0.12.1)
2026-09-06T13:09:01.8636758Z - Required stages: impl, unit
2026-09-06T13:09:01.8636792Z 
2026-09-06T13:09:01.8636887Z ### REQ-ENDPOINT-LIST-MERGE-LOCAL
2026-09-06T13:09:01.8638615Z - Title: `spt endpoint list` always merges this node's LOCAL (unadvertised) perches into the view; the `--local` flag is REMOVED (operator decision 2026-06-17). Rationale: `spt whoami` is a thin alias of `endpoint list` — a just-online agent running `whoami` must see its OWN perch, or it gets an omitted-self view ('chaos'). FIX: drop the `--local` flag + its `--detail` conflict test + the v0.10.0 REQ-PICKER-5 hint line (cli.rs:1678) + cmd_list_local; the bare list merges local perches into the subnet view; fix the whoami alias path accordingly. Run `cargo run -p xtask -- gen` (docs-drift, DEFAULT target). (v0.12.1)
2026-09-06T13:09:01.8638757Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8638791Z 
2026-09-06T13:09:01.8638920Z ### REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT
2026-09-06T13:09:01.8643936Z - Title: A clean `spt rc` attach to a LIVE spt-hosted (`endpoint run`) harness must DELIVER the harness's PTY output. KEYSTONE — the operator's central 'attach shows no output' symptom, reproduced on the real dummy-harness fixture (v0.12.1 Wave 1) with NO death and NO wedge: bringup succeeds (online, harness pid alive + heartbeating, psyche hosted), the attach CONNECTS (PUMP_IPC_READER spawned, no RC_FAIL, holds the full window) — but receives EXACTLY 0 bytes over 10s of the harness's flushed [session.self] stdout. DISTINCT from REQ-HAZARD-VIEWER-CLOSE-DETACH (death) and REQ-HAZARD-ATTACH-WEDGE (dead-child backpressure): here the harness is ALIVE and the attach is a clean first subscribe. This BLOCKS the 'view is independent' invariant — re-attach is meaningless if a live endpoint-run harness shows nothing. KNOWN-GOOD (rules out 'no drain'): attach.rs `local_attach_via_loopback_conn_rides_the_same_pump` + `broker_spawns_the_pty_child_in_the_requested_cwd` prove the broker DOES drain+fan a `spawn_session` PTY child to a loopback attach over the SAME transport rc uses. Both spawn_session and endpoint-run's spawn_session_pid send KIND_SPAWN → the same dispatch_spawn (broker.rs:706/835) which starts the per-session drain+OutputLog — so the gap is NARROWER than 'no drain', endpoint-run-specific. Root candidates: (a) spawn_session_pid's SpawnReq stdio/env/cwd differs so the dummy's stdout isn't the captured ConPTY; (b) the harness stdout WRITE BLOCKS because the ConPTY buffer fills (drain not reading THIS pty) — explains alive-but-0-bytes; (c) ConPTY reader-park (KH 7.6) on this path; (d) `spt rc` resolve_session/subscribe for an endpoint-run session subscribes to the wrong/empty log. (v0.12.1)
2026-09-06T13:09:01.8644128Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8644156Z 
2026-09-06T13:09:01.8644241Z ### REQ-CLI-HELP-MARKDOWN
2026-09-06T13:09:01.8647181Z - Title: `spt --help` (and every subcommand --help) renders the inline Markdown authored in the clap doc-comments as terminal styling, never as literal markers: `**bold**` → ANSI bold, `` `code` `` → ANSI cyan, `[text](url)` → `text`. The markers are STRIPPED either way — a raw `**` or backtick must NEVER reach the user (the operator-reported v0.12.0 defect: help text reads `**ctrl-b**` and stray backticks verbatim). Color/bold escapes are emitted ONLY when the help is going to a real terminal AND color is not suppressed (NO_COLOR unset · CLICOLOR != 0 · CLICOLOR_FORCE forces on); a pipe / redirect / CI / NO_COLOR falls back to strip-only (clean plaintext, zero escapes) so machine-readable help is byte-identical regardless of marker syntax. Pure transform over the clap-rendered help string at the single run()/bare_invocation chokepoint; preserves pre-existing ANSI (CSI sequences passed through untouched), never spans markers across a newline, leaves unmatched/empty markers literal, and does not alter the help layout. (v0.12.1)
2026-09-06T13:09:01.8647433Z - Required stages: impl, unit
2026-09-06T13:09:01.8647457Z 
2026-09-06T13:09:01.8647563Z ### REQ-HAZARD-WMI-DAEMON-WINDOW
2026-09-06T13:09:01.8650082Z - Title: `spt daemon start` launches the daemon with NO visible console window. REGRESSION (v0.12.1 L1.5): the WMI job-neutral launch (spawn_daemon_via_wmi) set CREATE_NO_WINDOW on the launching powershell but NOT on the Win32_Process.Create call — Win32_Process.Create does not inherit it, so the spawned cmd.exe env-forwarding wrapper popped a console window on every cold-start (violating REQ-INSTALL-10's v0.7.4 no-persistent-window invariant; the old detached_no_inherit path set DETACHED_PROCESS|CREATE_NO_WINDOW). FIX: pass a Win32_ProcessStartup with CreateFlags=DETACHED_PROCESS (0x8 — no console so no window; CREATE_NO_WINDOW 0x08000000 is NOT a valid Win32_ProcessStartup flag → ReturnValue 21 invalid-param, which is why the naive port fails) + ShowWindow=SW_HIDE(0) belt, via the ProcessStartupInformation argument. (v0.12.2)
2026-09-06T13:09:01.8650195Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8650228Z 
2026-09-06T13:09:01.8650324Z ### REQ-CLI-OUTPUT-MARKDOWN
2026-09-06T13:09:01.8654759Z - Title: Human-prose COMMAND OUTPUT (not just `--help`) renders the inline Markdown authored in its source strings as terminal styling, never literal markers: `` `code` `` → ANSI cyan, `**bold**` → ANSI bold, `[text](url)` → `text`, markers STRIPPED either way. REQ-CLI-HELP-MARKDOWN only hooked the clap `--help` chokepoint, so command output still printed raw Markdown (audit: `spt how-to` topic text showed `# headers`/backticks, `spt subnet`/`subnet status` hint footers showed stray backticks, the daemon-status `not running` line, the `ENDPOINT_RUN_STARTED` attach hint, and the daemon's `SUBNET_DETACHED` startup line — 13 prose surfaces). The same line-bounded pure `helpfmt::render` is applied at each emit site, color-gated by the OUTPUT STREAM's own tty (`stdout_color` for print/println, the new `stderr_color` for eprintln). HARNESS-SAFETY (binding): color is tty-gated, so an adapter (piped / non-tty / NO_COLOR) gets STRIP mode = zero ANSI + markers removed; every dual-contract MACHINE token on a rendered line (`ENDPOINT_RUN_STARTED:`, `NO_SUCH_TOPIC:`, `SUBNET_DETACHED:`) carries NO Markdown markers, so it survives strip byte-intact — the adapter parse is never perturbed. Pure-machine output (the `<EVENT …>` envelope, bringup parse-tokens SEEDED/BOUND/READY/NO_SEED, `--json`, QR) is NEVER routed through the renderer. The one spt-daemon source string (`SUBNET_DETACHED`, the bin-local renderer is unreachable from the daemon crate) is authored marker-free instead. (v0.12.2)
2026-09-06T13:09:01.8654974Z - Required stages: impl, unit
2026-09-06T13:09:01.8655002Z 
2026-09-06T13:09:01.8655106Z ### REQ-HAZARD-INJECT-CONTROL-COEXIST
2026-09-06T13:09:01.8662616Z - Title: SPINE INVARIANT (v0.13.0 keystone): the broker must accept INJECTED keystrokes into an spt-hosted PTY (the v0.11.0 raw direct-inject today; the ADR-0022 translation-binary choreography tomorrow) WHILE a live `spt rc` controller is attached to the SAME PTY, without (a) the operator losing control, (b) the endpoint latching ONLINE+CONTROLLED, or (c) the broker wedging. The injection inlet is PERMANENT — spt-claude-code requires keystroke injection — so this is root-caused + fixed at the PTY-injection layer, IN STEP with the ADR-0022 delivery redesign that formalizes the inlet. REOPENS the wedge facet of REQ-HAZARD-ATTACH-WEDGE: the v0.12.1 prove-don't-change covered only DEAD-CHILD backpressure, NOT the injection trigger (operator's signal — one injected keystroke succeeds, the next wedges → the single-threaded broker parks on a blocking PTY/loopback write after injection-induced harness output). REPRO-FIRST on the real dummy-harness fixture (NO theory): instrument to nail the exact blocking call before any fix. Fix candidates: non-blocking/fail-fast PTY write, split input/output, bounded-evicting. Mechanism shared with W2 — spt-core owns EVERY PTY write and applies an injected sequence ATOMICALLY (controller input buffered during the sequence, flushed after) so a stash/restore can't be clobbered. CONFIRMED ROOT (doyle /diagnose 2026-06-19, code-grounded): Broker::append (broker.rs:205-227) fans each live output chunk to the CONTROLLER on a SYNCHRONOUS BLOCKING write_frame held inline in the session's drain thread (the 'authoritative, advances delivered_through' path, D4-1), while VIEWERS use a dedicated writer thread + bounded evicting sync_channel (add_viewer:273 / viewer_writer) that can never stall the drain. So a slow/backed-up controller socket — or the full 64KB loopback duplex (the ATTACH-WEDGE buffer) — BLOCKS the drain thread → output stalls → keystroke echoes stall (PERCEIVED input lag) → unrecoverable wedge when the consumer never drains. TRIGGERS ON NORMAL INTERACTIVE rc USE under heavy harness output (TUI redraw), NOT only message injection — same root, wider repro. FIX DIRECTION: move controller delivery off the drain thread onto a dedicated writer (the viewer_writer pattern) BUT preserve the authoritative cursor — block the WRITER thread (not the drain), bound the wedge (deadline → detach/mark-gone, never park forever), never silently evict the operator's authoritative view. (v0.13.0)
2026-09-06T13:09:01.8662899Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8662927Z 
2026-09-06T13:09:01.8663088Z ### REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE
2026-09-06T13:09:01.8668330Z - Title: A SLOW controller must not starve a concurrent `rc --view` VIEWER. W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST) moved the controller SOCKET WRITE off the drain thread onto controller_writer, but left the bounded HANDOFF (ControllerJob::deliver) as an INLINE try_send SLEEP-POLL on the drain (broker.rs:1450-1457 → deliver:669-685, up to CONTROLLER_WRITE_DEADLINE=5s). So when a controller drains slower than the PTY floods, its CONTROLLER_CHANNEL_DEPTH(4096) channel fills, deliver() polls inline, and the DRAIN THREAD is throttled to the controller's read rate → OutputLog::append's viewer fan-out (try_send) stops running → a concurrent VIEWER receives only the initial replay then nothing (root 'b4', warm forkpty: a_journaled c1=0/EVICT=0/got_output=FALSE; steady-state-near-full = no recovery; forkpty-only, floods harder than Windows ConPTY). The viewer-not-starved-by-a-busy-session property is legitimate (rc --view of a noisy session must show LIVE output). FIX: the controller becomes a SINGLE NON-BLOCKING try_send (like a viewer), done IN append() under the log lock; deliver()'s sleep-poll DELETED; the drain NEVER sleeps. ControllerSink gains a stateful last_ok deadline → a TRULY-stalled controller (continuous-Full past CONTROLLER_WRITE_DEADLINE) is evicted (bounded-wedge preserved); a slow-but-alive controller DROPS frames + falls behind the ring (resume-from-floor, the existing reconnect case). B2 GAPLESS-HANDOFF PRESERVED via a CONTIGUOUS delivered_through: controller_writer advances the cursor ONLY when the written seq == cursor (next expected); a gap from a drop FREEZES the cursor at last-contiguous so a re-attaching brain's resume_seq never skips a dropped chunk (a high-watermark advance past the gap would be a not-exactly-once resume = B2 violation, doyle's gate). (v0.13.0)
2026-09-06T13:09:01.8668582Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8668616Z 
2026-09-06T13:09:01.8668721Z ### REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT
2026-09-06T13:09:01.8673992Z - Title: A `rc --view` VIEWER that overflows its broker subscription queue and is EVICTED (OutputLog::append try_send Full → viewers.remove, REQ-HAZARD-VIEWER-ISOLATION session-protection) must SKIP TO LIVE, not die silently. ROOT (v0.13.0, b4 JIT item 2 = p0_paste + post-b4 a_journaled-Linux, ONE root): serve_attach forwards each frame (read_event→b64decode→re-encode AttachRecord→net_stream_send) SLOWER than the drain fans out under flood → its VIEWER_CHANNEL_DEPTH(256) channel overflows → the drain evicts (viewers.remove drops the ViewerSink → drops tx → viewer_writer's rx.recv() Err → the writer returns WRITING NOTHING) → serve_attach's brain.read_event() just STOPS getting Output (no EOF, no error) → serve_attach blocks forever → the operator receives nothing (attach_received_output=FALSE). Eviction-of-a-hopelessly-behind-viewer is CORRECT session-protection (keep it); SILENT+PERMANENT eviction is the bug. VIEWER-only → B2-SAFE (a viewer never advances delivered_through / is not authoritative / exposes no resume cursor). FIX (doyle-gated, skip-to-live = tail -f reconnect): (1) explicit broker→viewer EVICTION SIGNAL (KIND_VIEWER_EVICTED, written in the viewer_writer thread OFF the log lock, DISTINCT from session-exit EOF so serve must NOT tear down on it); (2) serve_attach re-subscribes from the CURRENT ring floor (skip-to-live, replays nothing, sees the next live burst) — resetting the cold serve-brain's next_seq so the post-eviction forward-jump replay is accepted (the legacy reject-gap path, brain.rs:618-626, would otherwise FATAL the forward jump); (3) HARD constraint NO evict→resubscribe busy-loop: serve_attach rate-limits re-subscribes (RESUBSCRIBE_INTERVAL) so under max-flood the operator sees intermittent LIVE bursts, never a CPU spin. (v0.13.0)
2026-09-06T13:09:01.8674197Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8674225Z 
2026-09-06T13:09:01.8674326Z ### REQ-HAZARD-VIEWER-RING-ROLL-SNAP
2026-09-06T13:09:01.8678537Z - Title: A read-only rc --view VIEWER whose serving brain falls behind the live ring under a hard flood and receives a FORWARD Output seq gap (the ring rolled frames out between reads, BEFORE any channel-overflow eviction → NO KIND_VIEWER_EVICTED marker) must SNAP TO LIVE (accept-and-advance via dedup-below + snap-above), NOT fatal with output gap (brain.rs:624/628 legacy reject-gap). ROOT (v0.13.0 forkpty, post-b4+skip-to-live): serve_attach subscribes a viewer via brain.attach_as(Viewer) leaving session_cursors EMPTY → the viewer serve-brain uses the LEGACY reject-gap → a PRE-eviction ring-roll forward-gap FATALS read_event → serve_attach returns → forwarding stops → attach_received_pty_output=FALSE (a_journaled / p0_paste / attach.rs:1071 wedged_viewer, Linux forkpty; Windows ConPTY floods slower → MASKED false-green). DISTINCT from REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT (the POST-eviction re-subscribe-from-floor): this is PRE-eviction gap-tolerance while STILL subscribed. VIEWER-only → B2-SAFE (a viewer never advances delivered_through / is not authoritative); the CONTROLLER keeps strict reject-gap (exactly-once resume). FIX: arm snap-above at initial viewer attach (attach_as_viewer_snap = attach_as(Viewer) + session_cursors.insert(session_id, from_seq)); the two viewer-survival mechanisms COMPOSE — this tolerates pre-eviction ring-roll gaps, REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT recovers post-eviction. (v0.13.0)
2026-09-06T13:09:01.8678713Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8678742Z 
2026-09-06T13:09:01.8678851Z ### REQ-HAZARD-CONTROLLER-GAP-RESUME
2026-09-06T13:09:01.8685331Z - Title: A serving CONTROLLER whose serve-brain hits a b4 drop-don't-block FORWARD output gap must RESUME-FROM-FLOOR (re-subscribe from delivered_through and re-fetch the dropped frames from the ring), NOT snap-above and NOT fatal. ROOT (v0.13.0 forkpty re-run, post-keystone): b4 made the controller a non-blocking try_send that DROPS frames when its bounded channel fills (a controller that falls behind its OWN echo under a hard flood), so the next read is a forward gap the strict reject-gap (brain.rs:624/628, B2 exactly-once) FATALS — wedged_viewer_does_not_stall_controller (attach.rs:1048) drove ctrl.read_event() raw and fataled on `output gap got 6134 want 4643`. Pre-b4 the inline sleep-poll BLOCKED the drain to the controller's rate (no drops, no gaps); this is a b4 SIDE-EFFECT, not a new class. A controller CANNOT snap (it is authoritative — advances delivered_through; skipping rolled frames = not-exactly-once = B2 violation), so REQ-HAZARD-VIEWER-RING-ROLL-SNAP does NOT apply. B2 INVARIANT (doyle, broker.rs:327-330): the ring trim is delivered_through-BLIND (`while ring.len() > cap_chunks { pop_front() }`), so re-fetch is exactly-once IFF tail - delivered_through <= cap_chunks (4096) — NOT guaranteed in general, but the common case (burst < ring; wedged_viewer ~1492 < 4096) holds. FIX: serve_attach catches the output-gap on the controller path (does not ?-propagate) and re-subscribes from Brain::controller_resume_floor (= delivered_through = the gap's `want`; NO mid-stream KIND_SESSIONS round-trip — sessions() loops on read_event and would re-fatal on the same gap + discard Output); the broker replays the dropped frames. The IRRECOVERABLE edge (floor unchanged across two resumes = ring rolled past delivered_through = frames gone) surfaces a MARKED truncation to the operator (never silent-skip = B2 lie, never spin) and ends cleanly — full graceful handling deferred to REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND. Do NOT make the ring trim delivered_through-aware (that risks an unbounded ring under a stuck controller; the 5s eviction + 4096 ring is the practical bound). (v0.13.0)
2026-09-06T13:09:01.8685600Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8685629Z 
2026-09-06T13:09:01.8685762Z ### REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND
2026-09-06T13:09:01.8688539Z - Title: DEFERRED EDGE of REQ-HAZARD-CONTROLLER-GAP-RESUME: when a serving controller falls behind the live ring FURTHER than the ring holds (tail - delivered_through > cap_chunks, the ring trim being delivered_through-blind, broker.rs:327-330), the dropped frames have rolled OUT of the ring and an exactly-once re-fetch is IMPOSSIBLE. v0.13.0 DETECTS this (resume floor unchanged across two consecutive resumes) and surfaces a MARKED truncation notice to the operator then ends the attach cleanly — it never silent-skips (a B2 lie) and never spins. FULL graceful handling (a clearly-marked snap-with-data-loss that keeps the operator on the live tail, or a structured truncation record the rc renders distinctly, plus the controller-too-slow + ring-too-small backpressure/sizing policy) is DEFERRED — staging it needs a netsplit / deep-behind harness (the in-process loopback rig keeps up; wedged_viewer's gap is recoverable at ~1492 < 4096). (v0.13.0+)
2026-09-06T13:09:01.8688644Z - Required stages: 
2026-09-06T13:09:01.8688677Z 
2026-09-06T13:09:01.8688787Z ### REQ-MSG-IDLE-TRANSLATION-BINARY
2026-09-06T13:09:01.8694879Z - Title: spt-hosted idle message delivery via an adapter TRANSLATION BINARY (ADR-0022). New opt-in manifest section `[message-idle-translation-binary]` = a TABLE carrying a `path` scalar (doyle OPT-B ruling: modeled as a table, not a bare top-level scalar, so a preceding section cannot silently absorb it + N+1 extensible; spt-core does NOT deny_unknown_fields, so a future key degrades gracefully); spt-core LIFECYCLE-manages it (spawn when the endpoint comes up, terminate when it goes down). The binary is a PURE stdin→stdout filter; spt-core owns EVERY PTY write. stdin (JSON-lines): `{type:"init",endpoint_id,node}` first · `{type:"event",envelope:"<EVENT…>"}` per inbound message (ADR-0020 envelope) · `{type:"input"}` content-free ping on each operator keystroke (binary tracks user-idle for its own idle-gated buffering; PTY input content NOT duplicated). stdout (JSON-lines): keystroke-commands `{key:…}`/`{delay_ms:…}`/`{text:…}` (extensible). spt-core applies the emitted sequence to the broker PTY ATOMICALLY (the W1 coordination — REQ-HAZARD-INJECT-CONTROL-COEXIST). The daemon poll feed is the ONE idle substrate for both topologies (Q1=A): harness-hosted consumer = the Monitor child, spt-hosted consumer = this binary; spt-core PREFERS a perch's poll listener if one exists (so spt-hosted can run a listener AND keep `spt rc`). Idle-only; busy/mid-turn = adapter hook-injection. Closes the current grounding gap: `api bind` registers no listener port → a listener-less spt-hosted perch SPOOLS inbound (only spooling+adapter-poll works today) → this delivers real inbound into the PTY. AMENDED v0.14.3 (ADR-0022 amendment, raw-inject removal): idle delivery is translation-binary-ONLY — the v0.11.0 raw `{text:payload}{key:enter}` inject is NO LONGER a delivery path; with no working binary (absent/spawn-failed/faulted/worker-gone) the inbound SPOOLS (delivered=false, poll-fed, LOUD), never a raw PTY pseudo-write (which did not submit on a modern TUI — the silent degrade that masked F-019). See REQ-HAZARD-IDLE-SILENT-NONDELIVERY. (v0.13.0, amended v0.14.3)
2026-09-06T13:09:01.8695232Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8695261Z 
2026-09-06T13:09:01.8695365Z ### REQ-HAZARD-IDLE-SILENT-NONDELIVERY
2026-09-06T13:09:01.8705329Z - Title: An idle delivery to a session whose translation binary is in a FAILED STATE — absent (none declared), spawn-failed, FAULTED, or its inject-worker channel gone — must SPOOL (delivered=false), never raw-inject a pseudo-delivery reported as delivered. The GUARANTEE is the STEADY STATE (the failed-binary state), not every in-flight message (see the fault-transient carve-out below). ROOT (F-019 post-mortem, ADR-0022 amendment): the v0.11.0 path raw-injected `payload+\r` into the PTY whenever no working translation binary handled an inbound message (none declared, spawn-failed, FAULTED, or its inject-worker channel gone) AND acked `delivered=true` — but a bare `payload+\r` does NOT submit on a modern TUI (Claude Code), so the message was TYPED but never sent: a silent pseudo-delivery reported as success. That silent degrade-to-raw-inject is exactly what MASKED F-019 through a multi-hour black-box hunt. FIX (operator-ruled, doyle-scoped): idle delivery is translation-binary-ONLY — `dispatch_endpoint_input` with no working binary replies `endpoint_injected_envelope(ep, delivered=false)` (the caller `try_broker_inject`→`cmd_send` then falls through to `deliver::send` = SPOOL, poll-fed, never lost) and writes NOTHING to the PTY, LOUDLY (eprintln names the absent/faulted/worker-gone cause). A failed binary becomes a VISIBLE no-delivery (spooled + honest QUEUED report), never a confident-but-false 'Sent'. The raw-inject fallback (`input.enqueue`) is REMOVED from the no-binary, worker-dropped, AND post-fault paths. OUT OF SCOPE (doyle ruling, follow-up note only): broker-side auto-redrive of already-spooled inbound the instant a live-update binary spawns (ordering/exactly-once hazards; the poll substrate + subsequent sends cover re-delivery). NOT COVERED — the FAULT-TRANSIENT (the STATE-vs-transient precision): a delivery landing in the worker's commit window — BEFORE `event_rx` is dropped / `faulted` is set — can be optimistically enqueue-acked (`delivered=true` the instant `event_tx.send` succeeds) then DROPPED when the worker faults+returns. That is a SEPARATE, PRE-EXISTING hazard: raw-inject removal did not touch it (the old code dropped that queued event too) — v0.14.3 is a strict improvement that makes nothing worse. It is tracked for v0.15.0 under REQ-MSG-DELIVERY-AXES (the spool-centric delivery redesign: ack-on-SPOOL replaces ack-on-enqueue, which closes the optimistic-ack drop naturally). v0.14.3 guarantees only the steady FAILED state → spool (faulted is MONOTONIC — set once, never respawns — so it converges deterministically; the g2 gate asserts the steady state via bounded-retry-until-spool, not a single-shot ack). EPHEMERAL CARVE-OUT (v0.15.0 W3, ADR-0028): `--ephemeral` is the SOLE sender-opted-in exception — an ephemeral message MAY drop silently if it cannot deliver in its accepted window (at window-open with no live carrier, or at TTL). Every NON-ephemeral path still spools + reports `delivered=false` (the guarantee is unchanged for the default durable path). v0.15.0 realizes the ephemeral drop for the spt-hosted-binary no-carrier-at-window leg + TTL; the harness-relay no-live-listener leg is a documented partial (CONTEXT.md §persistence). KNOWN-HAZARDS class (rule 4). (v0.14.3; ephemeral carve-out v0.15.0)
2026-09-06T13:09:01.8705687Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8705715Z 
2026-09-06T13:09:01.8705812Z ### REQ-MSG-DELIVERY-AXES
2026-09-06T13:09:01.8712455Z - Title: Activity-gated inbound delivery + per-message send control as THREE ORTHOGONAL AXES plus opaque metadata (ADR-0028; grilled w/ operator 2026-06-23). SUBSTRATE (the legacy-SPT parity gap, scaffolded-but-unwired today: `delivery::is_idle` + `resolve_inject_methods` exist but the result is discarded `let _methods`, and `broker::dispatch_endpoint_input` injects unconditionally — its comment calls activity-gating 'a deferred follow wave'): an inbound message has an ACTIVE window (endpoint active → spool for the receiver's hook-poll, non-disruptive) and an IDLE window (idle/idle-transition → deliver immediately: translation binary spt-hosted → relay-poll either topology → spool, in fallback order). AXES (each composes; each defaults to its unrestricted value): (1) DELIVERY WINDOW — default (both, first-to-fire) | `--idle-only` (idle window; immediate if already idle) | `--active-only` (active window only, never wakes; the RENAMED `--deferred` — `deferred=1` spool column + `api poll --include-deferred` keep their names). (2) CHANNEL RESTRICTION — unrestricted | `--prefer-native` (translation binary if running else fall back) | `--force-native` (binary ONLY, no fallback/no spool-to-other-method). Native flags do NOT respect the binary's idle-gating: the WINDOW says when, the native flag says through-what (so `--force-native --active-only` = binary injects during the active window, mid-turn-safe via the existing InjectFloor). (3) PERSISTENCE — durable (default; spool until delivered or TTL) | `--ephemeral` (drop if undeliverable in the accepted window — at window-open with no live carrier, or at TTL, whichever first). METADATA (orthogonal): `--json-payload '<json>'` → a single attr-escaped `json="…"` envelope attr ALONGSIDE (not replacing) the body, pure verbatim passthrough across spool/TCP/WAN/EVENT-PART, parsed only by the receiving adapter; collision-proof by construction (structured data lives INSIDE the one `json` value, can never forge `from`/`type`); available to ANY sender (confers no spt-core authority). HAZARD: `--ephemeral` is the ONLY path permitted to drop silently — the sender-opted-in carve-out to REQ-HAZARD-IDLE-SILENT-NONDELIVERY (that hazard gains a '…unless --ephemeral' clause in v0.15.0). (v0.15.0)
2026-09-06T13:09:01.8712608Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8712640Z 
2026-09-06T13:09:01.8712740Z ### REQ-RESUME-CONTEXT-PULL
2026-09-06T13:09:01.8724356Z - Title: Adapter-callable resume-context pull verb + not-yet-synthesized commune/signoff drop append (legacy-SPT parity, operator-directed 2026-06-24). GAP: spt-core exposes NO verb for a harness adapter's SessionStart hook to pull an agent's resume context — `resume::download_psyche_context` (spt-live/src/resume.rs:88, composes <live-role>+<live-context>+<project-context> from the durable two-tier store) is INTERNAL with ZERO spt callers and no ApiCmd verb (api/mod.rs ApiCmd enum has none); resume.rs:9 documents the intended 'adapter pulls it in its SessionStart hook' path but it was NEVER wired. Result: a harness adapter cannot inject the agent's durable mind on resume at all (claude-spt today runs only `api boundary` session-rotation + an identity brief — the agent resumes WITHOUT its mind). TIER-1 SCOPE (operator-approved; Tier-2 = drift-stamp/<current>/drift-directive + <memformat> + Pulse-Log DEFERRED to a separate parity item, NOT v0.15.0 — the legacy download_payload [claude_skill_owl context.rs:344] is richer but memformat is roadmap-deferred + drift-stamp is an orthogonal cross-machine-drift feature). TWO PARTS: (1) EXPOSE `spt api psyche-download <id> [--session-id <sid>]` -> stdout = the composed brief, project_id resolved from the endpoint's bound cwd (info::read_info -> cwd -> project derive; NO --project arg), auth-gated like sibling id-scoped verbs (the `gated(&id,&auth,…)` pattern); empty store -> NO-CONTEXT on stderr (mirror legacy). The adapter SessionStart hook runs it + injects stdout as additionalContext. (2) APPEND any commune/signoff drop NOT YET SYNTHESIZED into the durable tiers as a distinct <pending-commune>/<pending-signoff> slice AFTER the durable slices. GATING (operator ruling): append while NOT-YET-SYNTHESIZED, NOT merely 'while the raw file is on disk' — in today's synchronous ingest (ingest_drops route_two_slice writes durable THEN deletes the file, lifecycle.rs:466 @ DEFAULT_PULSE_PERIOD 5s) the two coincide (a watched-dir drop IS pre-synthesis), so the v1 realization reads the manifest-declared session.commune_dir/signoff_dir (manifest.rs:208/210) for a present <id>-commune.md/<id>-signoff.md (COMMUNE_SUFFIX/SIGNOFF_SUFFIX, ingest.rs); the CONTRACT keys on synthesis-state so it stays correct when async Psyche synthesis lands (a consumed-but-not-yet-committed drop stays appended via a pending-synthesis staging set — forward hook). The agent-checkpoint trigger sentinel CHECKPOINT_SENTINEL=`!!checkpoint!!` (a FIXED spt-core constant — operator-specified, CONTEXT.md §fixed-constants, NOT adapter-configurable) is stripped at BOTH drop-body points via one shared `strip_checkpoint_markers` (remove every token, keep inter-marker text, collapse trivial whitespace): the PRE-synthesis pending-append (resume::append_pending) AND the POST-synthesis durable ingest (ingest::route_slices — the single choke covering route_two_slice + signoff.write_resume_commune; strip-then-empty-filter so a marker-only slice routes nowhere) — else the marker would persist PERMANENTLY in live-context.md once a checkpoint drop synthesizes + re-trigger once the adapter's checkpoint detection is live. PRESENTATION-ONLY: the append NEVER writes the durable store (spt-core remains sole store-writer, REQ-HAZARD-DROP-FILE-SINGLE-WRITER; mirror legacy's read-only/process_file_drop-sole-deleter discipline). SELF-CLEARING: once synthesis commits the <pending-*> slice vanishes — no duplication. CORE-OWNED (not adapter): an adapter-side raw-file read RACES spt-core's ingest-delete (TOCTOU, ingest.rs:161 removes the drop on pulse-consume); the fold MUST live in the single composer all resume pulls flow through. New public CLI verb -> docs-drift gate (xtask gen + reference.md no-internal-codes, cli-command-docs-drift). (v0.15.0 parity wave W5)
2026-09-06T13:09:01.8724732Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8724760Z 
2026-09-06T13:09:01.8724863Z ### REQ-ADAPTER-TRANSLATE-PROOF
2026-09-06T13:09:01.8729233Z - Title: `spt adapter translate-proof <adapter> --event <envelope> [--session <id>]` — the author-time EMIT-half proof tool for `[message-idle-translation-binary]` (ADR-0022), symmetric to `spt adapter digest-proof` (REQ-TERM-5). It spawns and feeds the adapter's declared translation binary EXACTLY as the daemon does at idle-delivery — running the REAL `spt_daemon::translation` driver VERBATIM (no protocol reimplementation): `TranslationChild::spawn` the binary, send the `{type:"init",endpoint_id,node}` line then the `{type:"event",envelope}` line, and read back the emitted `{key}`/`{text}`/`{delay_ms}`/`{commit}` keystroke-command stream — then prints it author-readable (each Key with its `key_to_bytes` rendering, Text quoted, Delay in ms, Commit marker) with counts. It fills the SAME `{id}`→option and `{session_id}`→(--session, else a placeholder) keys into the `--event` envelope the daemon fills at runtime, so an envelope that proofs here feeds faithfully live. EMIT-half ONLY: it proves the binary's spawn+feed+emit contract; it does NOT exercise the daemon's atomic PTY apply / controller-buffering (that stays covered by the W2 inject_control_wedge int gate) — `--help` says so. Exit codes mirror digest-proof: 0 ok, 1 on spawn-fail / zero commands / no-commit-or-output / unparseable, 2 when the adapter declares no `[message-idle-translation-binary]` section. The `TranslationChild` Drop does the bounded no-zombie reap. (v0.13.x)
2026-09-06T13:09:01.8729543Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8729576Z 
2026-09-06T13:09:01.8729676Z ### REQ-HAZARD-BIND-CWD-UNSET
2026-09-06T13:09:01.8732238Z - Title: A bound endpoint's `info.cwd` is SET at bind so a freshly-created perch appears under its own project tab. ROOT (found, v0.13.0): `info.cwd` is NEVER set on bind — `cmd_bind` (spt-hosted) and `bind_from_seed` (harness-hosted) never thread cwd into `establish_perch`/`rec.cwd`. FIX: `cmd_bind` reads its own `current_dir` (the broker spawned it in `project_cwd`); `bind_from_seed` passes `seed.cwd` (already captured at seed time, currently DISCARDED). DISTINCT from REQ-PICKER-HISTORY-FRESH (v0.12.1) — that unioned cwd-origin into picker MEMBERSHIP but tested merge_origin_project with a PROVIDED origin; it never asserted `info.cwd` is actually set on bind, so a real `endpoint run` perch still had an empty cwd and the union had nothing to union. This is the v0.12.1 P1 'appears under its own project right away' claim that was REFUTED in the changelog — delivered for real here. (v0.13.0)
2026-09-06T13:09:01.8732359Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8732387Z 
2026-09-06T13:09:01.8732481Z ### REQ-PICKER-UX-V013
2026-09-06T13:09:01.8734081Z - Title: `spt endpoint run` picker UX (v0.13.0 operator dogfooding): (1) SKIP the first screen — open directly on 'Pick existing'; `n` jumps to 'Create new'. (2) AUTO-ATTACH after both Start-new AND Resume-from-history (both currently don't attach and show no stdout); add an `h` shortcut to run headless (no attach). (3) 'controlled by' shows the node NAME (node_label_display), not the raw hex. (4) Clean up Start-new output — drop the Rust `pid=Some(142748)` leak and the 'harness binds its perch on startup' internals; user-friendly, not a process log. (v0.13.0)
2026-09-06T13:09:01.8734185Z - Required stages: 
2026-09-06T13:09:01.8734209Z 
2026-09-06T13:09:01.8734314Z ### REQ-HAZARD-DRIVEN-BY-SELFHEAL
2026-09-06T13:09:01.8735865Z - Title: An spt-hosted endpoint's ONLINE+CONTROLLED state (`driven_by`) must CLEAR even when the detach IPC is lost — do NOT rely on the detach signal (same lesson as REQ-HAZARD-HOSTED-LIVENESS-RECONCILE B2): the reconcile loop clears `driven_by` when the endpoint has no live controller/session. Today a wedged or lost pump never delivers the detach, so the endpoint stays latched CONTROLLED forever. Composes with W1 (the wedge no longer blocks the detach) and rides the same pull-primary reconcile substrate as B2. (v0.13.0)
2026-09-06T13:09:01.8735970Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8735998Z 
2026-09-06T13:09:01.8736108Z ### REQ-RC-DRIVER-READ-LIVENESS
2026-09-06T13:09:01.8744794Z - Title: The rc client's busy-refuse reads a LIVE driver, never a stale stamp (kin to REQ-HAZARD-DRIVEN-BY-SELFHEAL / KH 7.15, observed from the CLIENT side; instrumented x20 2026-08-19, doyle-ruled same day). MECHANISM, measured not inferred: `driven_by` is broker-owned and EVERY clear of it lives in the daemon (broker stamp_reaped, converge_perch_stamps, the livehost reconcile belt), while the write that makes a stopped perch read `offline` is `terminal_normalize` — run SYNCHRONOUSLY in the `spt endpoint stop` CLI process, whose write set is exactly the REST triple (status / rest_state / dormant_since_ms) and structurally cannot touch the CONTROL triple. `current_driver` (rc.rs) was a raw disk read of `driven_by` with NO liveness check, feeding `pre_broker_busy_guidance`, which prints and `return Ok(())` BEFORE any broker traffic — so between the CLI's stop-write and the next daemon tick, a plain `spt rc` is refused by a controller that is provably gone, and the broker's own truth (no live session) is never consulted. This is operator-visible with no test involved: stop an endpoint, immediately rc it, get 'controlled by another window on this machine' and exit 0. Instrument vector (run-3 of 20): driven_by=Some(own hex) + controlled=true beside status=offline at the refusal instant, cleared only ~56s later — the clear is LATE, not missing. FIX = reader-side and NARROW: the driver a record names is `None` exactly when `status == STATUS_OFFLINE`, the product's own declared staleness marker (the same predicate the daemon's DRIVEN_BY_SELFHEAL_OFFLINE belt asserts at livehost.rs); the reader stops having to wait for the tick to agree. Any other status — INCLUDING a record with no `status` field at all — is left alone and reports its driver verbatim: a broader `!= online` rule would suppress the refusal on legitimately mid-bind records, which is worse than the residual it closes (3/20 status-absent reads measured at capture instants, doyle-ruled ACCEPTED WITH ITS NUMBER 2026-08-19; re-opens only if a gate-instant absent-status refusal is ever measured). NO new writer: `driven_by` stays single-writer (the broker) — the belt that would have cleared the control triple from the CLI stop path was ruled OUT for exactly that reason, and `terminal_normalize`'s doc instead states the ownership split by replacement. `--view`/`--take`/qualified targets are unaffected (they already bypass the gate). Gate: impl — the liveness-filtered driver read in rc.rs + the terminal_normalize ownership-split doc; unit — an offline record reports no driver, an online one reports it verbatim, and NEITHER a non-offline status NOR an absent status is treated as a staleness marker (the narrowing is asserted in both directions, so a later widening cannot land silently). NO int: the existing engine-room bring-up e2e already expresses the invariant and must go green as it stands — a new int cell would only re-run it.
2026-09-06T13:09:01.8745232Z - Required stages: impl, unit
2026-09-06T13:09:01.8745264Z 
2026-09-06T13:09:01.8745384Z ### REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT
2026-09-06T13:09:01.8750249Z - Title: An spt-hosted endpoint driven by a REMOTE controller whose remote is gone but whose broker connection stays OPEN (a wedged/lost pump that never delivers the detach) AND whose session is IDLE (no output) stays latched ONLINE+CONTROLLED forever: the W1 drain-evict only fires on OUTPUT (CONTROLLER_WRITE_DEADLINE on a backed-up write), a clean disconnect self-heals via detach_if→clear_controller, but an idle session with a half-open/wedged controller connection produces neither signal. PROVED repro-first on a real broker (v0.13.0 W5, inject_control_wedge.rs w5_a2): controller_by STAYS Some(origin) and driven_by STAYS Some after the remote is abandoned without a clean EOF on an idle session — so the brain reconcile CANNOT detect it from KIND_SESSIONS controller_by (the broker still reports it controlled). FIX DIRECTION (doyle ruling 2026-06-19, broker-side single-writer — the broker owns driven_by/clear_controller): wire the EXISTING D4c NetPresence connection-disconnect event → clear_controller for any session whose controller identity == the dead origin (become_controller already stores Some(origin); presence events already exist — modest wiring, NOT a new probe). The liveness ORACLE is QUIC's own keepalive/idle-timeout: a presence-disconnect IS a real QUIC conn close, already tolerant of transient blips within the keepalive window, so NO heavy partition ADR is needed UNLESS the QUIC timeout proves too slow for the UX (then mint an ADR for a faster controller-heartbeat + its false-evict bound). Composes with W1 (output path) + W5 Gap B (no-session) — this is the third, idle-remote, leg. (v0.13.0 follow-up)
2026-09-06T13:09:01.8750434Z - Required stages: int
2026-09-06T13:09:01.8750463Z 
2026-09-06T13:09:01.8750569Z ### REQ-HAZARD-RC-INPUT-KEY-ENCODING
2026-09-06T13:09:01.8755025Z - Title: An `spt rc` session forwards the Backspace key as the VT DEL byte (0x7f), so the hosted TUI (Claude Code) deletes ONE character — never a whole word. SYMPTOM (operator dogfooding): Backspace in an rc session always behaves like ctrl+Backspace — deletes the entire last word. ROOT (doyle /diagnose, code-grounded, byte PENDING HITL confirm): rc is a RAW VERBATIM byte pump — spawn_stdin_reader (rc.rs:152) reads std::io::stdin() bytes under crossterm raw mode and forwards them unchanged (parse_stdin_chunk only intercepts the ctrl-b detach prefix); there is NO key-event encoding and NO 0x08↔0x7f normalization ANYWHERE in the tree (grep: zero SetConsoleMode / ENABLE_VIRTUAL_TERMINAL_INPUT). On Windows, crossterm enable_raw_mode does NOT set ENABLE_VIRTUAL_TERMINAL_INPUT, so the LEGACY console delivers ^H (0x08, ctrl+h) for Backspace instead of VT DEL (0x7f); Claude Code maps ^H → backward-kill-word → the observed whole-word delete. CONFIRM-FIRST (build the loop): an env-gated hexdump in spawn_stdin_reader (SPT_RC_DEBUG_KEYS) prints the forwarded byte; operator presses Backspace + ctrl+Backspace in a real rc session. FIX CANDIDATES: (a) enable ENABLE_VIRTUAL_TERMINAL_INPUT on the rc stdin console on Windows so the console emits proper VT (Backspace→0x7f, arrows/Home/End as CSI) — cleanest, fixes the whole key map not just Backspace; (b) narrow normalize bare 0x08→0x7f in the rc input path (riskier — a real ctrl+h is also 0x08). Prefer (a) unless it regresses other keys. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)
2026-09-06T13:09:01.8755291Z - Required stages: impl, unit
2026-09-06T13:09:01.8755320Z 
2026-09-06T13:09:01.8755430Z ### REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE
2026-09-06T13:09:01.8764556Z - Title: The effect journal serializes EVERY PTY effect under one mutex held ACROSS two fsyncs AND the blocking PTY write — so interactive input stutters and ultimately wedges the daemon hard. ROOT (doyle /diagnose, code-grounded + MEASURED on the operator's real Windows box, 2026-06-19): EffectJournal::apply_once (effect.rs:168-188) takes `inner.lock()` and holds it across `write_line(PENDING)` → `effect()` → `write_line(DONE)`, where write_line (effect.rs:235-239) does flush()+sync_all() (a full FlushFileBuffers) — so each effect pays TWO fsyncs under a GLOBAL lock, and the closure `effect()` (the actual PTY write, broker.rs:1257 EffectKind::PtyWrite via attach.rs:197 send_effect) runs while the lock is held. Two operator-visible facets, ONE root: (A) STUTTER/LAG — every keystroke is a PtyWrite effect = 2× sync_all serialized; measured fsync on %LOCALAPPDATA%\spt-core = median 6.5ms, spikes to 198ms (C: was recently at 100%), so ~13ms+ per keystroke best case, hundreds under contention → 'many but not all keypresses take 100s of ms, choppy, worsens with volume'. (B) HARD PERMANENT WEDGE — when a PtyWrite `effect()` blocks (ConPTY input buffer full / harness not draining stdin), the journal lock is held INDEFINITELY → the single-threaded inbound-stream dispatch (dispatch.rs serve_attach, which both applies input effects AND opens attaches) can never progress → EVERY subsequent attach (`spt rc --view`/`--take`) fails with 'attach request: brain IPC read deadline elapsed' (confirmed: two retries deadline identically; broker control-plane KIND queries still answer — different thread). This REFUTES the W2-deferred ruling that park-(b)/(c) is 'Windows-benign because ConPTY absorbs 4MiB' — on the real box the input path wedges regardless. DISTINCT from W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST = the OUTPUT drain, correctly fixed @8b5583e; output uses broker.rs:1106 append, NOT the fsync journal). This is the INPUT/effect-journal path W1 never touched, and it is THE wedge the operator hits with --take/--view. FIX DIRECTION (candidates, repro-first — extend inject_control_wedge.rs to a REAL backed-up-PTY-consumer + a real rc-client attach assertion, the gap W1's gate missed): (1) do NOT hold the journal lock across effect() — reserve the key + fsync PENDING under lock, RELEASE, run effect(), re-acquire to fsync DONE + mark applied (preserve crash-idempotency via the per-key reservation, not a global hold); (2) bound/fail-fast the PtyWrite itself (the W2-deferred park bound — write_input must never block indefinitely, DSR-answer must not hold the writer mutex across a blocking write); (3) drop per-keystroke fsync on the interactive path — PtyWrite effects are EPHEMERAL (a keystroke lost on a broker crash is retyped; PTY state is not reconstructed from keystroke replay), so in-memory applied-set dedup suffices (the broker survives the brain — that IS the dedup anchor), with async/batched fsync or no-fsync for EffectKind::PtyWrite while durable kinds (NetSend/NetDial/Registry/Spool) keep their fsync. Combine (1)+(3) at minimum. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)
2026-09-06T13:09:01.8764902Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8764931Z 
2026-09-06T13:09:01.8765037Z ### REQ-HAZARD-RC-ATTACH-ONLINE-RACE
2026-09-06T13:09:01.8768604Z - Title: `spt endpoint run` in an ATTACH/VIEW terminal action attaches BEFORE the freshly-spawned endpoint is online, so the attach races (or outright loses to) the harness bind. ROOT (doyle /diagnose, code-grounded): cmd_endpoint_run (cli.rs) does launch_harness_brokered_in -> (if start: return) -> run_attach with NO await-online between them. launch_harness_brokered_in returns once the harness PROCESS is spawned, but the broker-PTY bind (info status -> STATUS_ONLINE + the live session) lands ASYNC. Both picker attach paths route here with start=false (RunMode::Attach -> cmd_endpoint_run start=false,view=false): Start-now catches the endpoint mid-bringup -> run_attach attempts + loses the handshake race; Resume-from-history catches it still fully OFFLINE -> run_attach's status-gate (REQ-HAZARD-RC-ATTACH-FAILFAST) short-circuits 'offline - nothing to attach' and NEVER attempts. SAME root, two faces (the W4 attach-by-default surfaced both; an online endpoint is unaffected - the picker returns Outcome::Attach, not Run). FIX: in cmd_endpoint_run, when the terminal action is attach/view (NOT start), AWAIT the endpoint online between launch_harness_brokered_in success and run_attach - poll spt_store::info read_info().status to STATUS_ONLINE with a bounded harness-boot deadline (~25s) at a tight interval; on online -> run_attach; on timeout -> ENDPOINT_RUN_ONLINE_TIMEOUT err (do NOT attach a dead bringup). (v0.13.0)
2026-09-06T13:09:01.8768728Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8768756Z 
2026-09-06T13:09:01.8768848Z ### REQ-RC-KEY-VT-TRANSLATE
2026-09-06T13:09:01.8774627Z - Title: On Windows, `spt rc` translates CONSOLE KEY EVENTS to standard xterm VT so ALL keys reach the hosted harness — arrows/Home/End/PgUp/PgDn/Insert/Delete/F-keys, every modifier combo, Backspace/Ctrl+Backspace — not just the byte-emitting ones. ROOT (operator HITL, doyle /diagnose): `spt rc` reads raw STDIN BYTES (spawn_stdin_reader, std::io::stdin().read); on the Windows LEGACY console (no ENABLE_VIRTUAL_TERMINAL_INPUT) the special keys produce console KEY_EVENTs, NOT stdin bytes, so the byte-pump sees nothing → those keys are DEAD. Enabling ENABLE_VIRTUAL_TERMINAL_INPUT was rejected (W7 dc07c39): on Windows Terminal it yields harness-specific win32-input-mode + broke ctrl-b detach. FIX (agnostic, full fidelity): on Windows, replace the stdin byte-read with a crossterm EVENT source (crossterm 0.28 already a dep; the picker already reads events) and translate each KeyEvent → STANDARD xterm VT bytes via a PURE translate_key_event(KeyEvent)->Vec<u8> (copy a known-correct xterm table verbatim, ADR-0001 spirit), forwarded through the SAME rc pump — the harness receives ordinary xterm VT (harness-AGNOSTIC, no win32-input-mode). Press-only (drop Repeat/Release). Detach stays the ctrl-b+'d' PREFIX, event-sourced (doyle Option B): Ctrl+B arms; armed+plain-'d'⇒Detach; armed+Ctrl+B⇒emit literal 0x02; armed+other⇒0x02 then translate(other). Non-tty stdin (piped/tests) → FALL BACK to the byte-read path (keeps e2e byte-injection working). UNIX UNCHANGED (its raw-mode byte stream already delivers proper VT; cfg-split, zero Unix regression). SUPERSEDES the W7 normalize_key_byte swap on Windows — the translator emits 0x7f for Backspace and 0x08 for Ctrl+Backspace natively (REQ-HAZARD-RC-INPUT-KEY-ENCODING folded in). NO int (a live interactive console can't be driven in CI — HITL, REQ-RUN-PICKER/RC-1 precedent); the exhaustive non-vacuous translate_key_event mapping unit + the event-detach unit ARE the surface. (v0.13.0)
2026-09-06T13:09:01.8774895Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8774928Z 
2026-09-06T13:09:01.8775088Z ### REQ-HAZARD-PTY-INPUT-WRITER-WEDGE
2026-09-06T13:09:01.8781380Z - Title: Pasting into an `spt rc` session WEDGES the broker — after a paste the operator can no longer type AND can no longer attach to NEW or EXISTING sessions (`brain IPC read deadline`). ROOT (doyle /diagnose, code-grounded): the operator-keystroke path rc -> net-stream Input -> serve_attach (attach.rs:197 brain.send_effect) -> KIND_INPUT -> broker dispatch loop (broker.rs:1091) -> dispatch_input (broker.rs:1459) -> session.write_input(&bytes) runs SYNCHRONOUSLY on the broker request-handling thread. W1b (REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE) released the journal lock across the effect (fix 1) + made PtyWrite ephemeral/no-fsync (fix 3) but EXPLICITLY DEFERRED fix (2) — bound/fail-fast the PtyWrite itself. A single keystroke never fills the ConPTY input buffer; a PASTE BURST does -> write_input blocks -> the dispatch thread cannot service the next frame (a re-attach subscribe, a become_controller restore-write, an inject-floor flush) -> wedge. Not a bug-2 regression (the byte path funnels to the same write_input; paste just reliably fills the buffer). FIX (doyle design, V0.13.0-P0-PTY-INPUT-WRITER-DESIGN.md, CONTEXT L33 broker-owns-PTY/minimal + L435 SessionSurface + single-writer pattern): one dedicated per-session INPUT-WRITER THREAD = the SOLE caller of the blocking write_input, fed by a BOUNDED FIFO channel; every caller (dispatch_input, serve_attach->send_effect, inject-floor flush) ENQUEUES + returns immediately, never blocks. A blocked/slow harness blocks ONLY its own writer thread, never the broker dispatch. Backpressure (operator ruling): queue full => DROP excess input + stamp the session INPUT_BACKPRESSURE (visible health signal); the daemon NEVER wedges; a merely-slow harness self-heals as the writer drains. Exactly-once preserved (PtyWrite ephemeral: apply_once effect = the non-blocking enqueue => Applied; ack now means accepted+ordered, benign — rc does not gate on landing); order preserved (single FIFO + single writer); inject-floor (W2 Layer C) choreography moves to the lone writer. Completes the W1b-deferred fix (2), cross-platform (cfg(unix) forkpty park folds in). (v0.13.0)
2026-09-06T13:09:01.8781513Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8781543Z 
2026-09-06T13:09:01.8781644Z ### REQ-SESSION-RESUME-TEMPLATE
2026-09-06T13:09:01.8788424Z - Title: Resuming an endpoint session that HAS conversation history brings up a BLANK session. ROOT (doyle, code-grounded + CONTEXT — case-3 spt-core MISSING feature, NOT a perri docs-miss): CONTEXT L127-129 already defines the resume-session seam ('continue-existing: resume an existing harness session under the adapter — its NATIVE resume'), and the manifest already has the resume-variant pattern (Session has BOTH psyche_init AND psyche_resume, manifest.rs:217-219) — but the agent's own session has ONLY self_ (`[session.self]`, no resume sibling). cmd_endpoint_run (cli.rs:1304) re-passes the session_id through `[session.self]` on resume (resume.unwrap_or_else(mint_session_id)), so the adapter's FRESH command (e.g. `claude --session-id ..`) runs again instead of the harness NATIVE resume (`claude -r ..`) -> CC starts a fresh transcript -> blank. spt-core forwards session_id + cwd faithfully; it just has no way to express the native-resume invocation. SECOND GAP: CC resolves a transcript by session_id + cwd, but the session ledger records only {ts, session_id, trigger} (no cwd), so picker Resume-from-history (cross-project rows) can't restore the right cwd. FIX (doyle design, V0.13.0-P2-SESSION-RESUME-DESIGN.md, mirrors psyche_init->psyche_resume exactly): (A) add a `[session.resume]` role (resume: Option<SessionRole> on Session + roles()/is_empty()); cmd_endpoint_run selects it when --resume is set AND it's declared (fill {id}/{session_id}=resumed id/{session_name} + the resume cwd), else FALL BACK to `[session.self]` (full back-compat). (B) record cwd PER ledger row (operator ruling): {ts, session_id, trigger, cwd} additive serde-default; resume cwd = resumed row cwd -> else perch info.cwd -> else current_dir (back-compat for old rows + single-project endpoints); picker threads the selected row's cwd through Outcome::Run -> cmd_endpoint_run. (C) public docs (MANIFEST + harness-contract) teach `[session.resume]` so perri builds the adapter side BLIND. Adapter follow-on (perri, AFTER spt-core ships+docs): declare `[session.resume] command = claude -r {session_id} --remote-control {id} --dangerously-skip-permissions` from the resume cwd. Completes REQ-READY-AGENT-RESUME / REQ-RUN-PICKER resume-from-history. (v0.13.0)
2026-09-06T13:09:01.8788771Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8788804Z 
2026-09-06T13:09:01.8788899Z ### REQ-RC-WIN-PASTE
2026-09-06T13:09:01.8793644Z - Title: In an `spt rc` session neither ctrl+V nor right-click pastes (CC explicitly supports ctrl+V). ROOT (doyle /diagnose): RawGuard does only enable_raw_mode (no bracketed paste / no mouse capture / no clipboard interception); the Windows console delivers a paste as synthetic per-char KEY EVENTs (no crossterm Event::Paste), and ctrl+V translates to bare ^V forwarded to CC — but CC runs DAEMON-SIDE with NO access to the operator's LOCAL clipboard, so remote paste is fundamentally CLIENT-ORIGINATED. A multi-line paste-as-keys also becomes a \r submit-storm. FIX (doyle design, V0.13.0-P1-RC-PASTE-DESIGN.md, cfg(windows), folds into the bug-2 event path): on a paste gesture rc reads the LOCAL clipboard + forwards a BRACKETED PASTE (ESC[200~ + content + ESC[201~); CC has bracketed-paste mode on (its TUI sets ESC[?2004h) so it treats it as a paste — content intact, no submit-storm, harness-AGNOSTIC. ctrl+V: intercept Char('v')+CONTROL in the event loop -> read_clipboard -> bracketed paste. Right-click: RawGuard also EnableMouseCapture (disables console QuickEdit + enables ENABLE_MOUSE_INPUT so right-click surfaces as Event::Mouse on legacy cmd/powershell) -> right-button -> read_clipboard -> bracketed paste; DROP all other mouse (CC has no mouse features, operator-confirmed, so capture costs nothing). read_clipboard = clipboard-win crate (cfg(windows), minimal); empty/failed = clean no-op. Content forwarded VERBATIM (literal pasted text, no per-char translation). Unix UNCHANGED (its terminal pastes natively through the byte pump). DEPENDS ON P0 (a paste chunk must not wedge the broker). (v0.13.0)
2026-09-06T13:09:01.8793773Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8793807Z 
2026-09-06T13:09:01.8793912Z ### REQ-HAZARD-INPUT-ACK-BACKPRESSURE
2026-09-06T13:09:01.8801300Z - Title: A FLOOD of operator input on one brain↔broker connection deadlocks the broker PERMANENTLY (entire broker — no new/existing attach; the controller stays latched because the per-conn handler can't process the detach). ROOT (doyle /diagnose, code-grounded + HITL capture, the v0.13.0 P1 ctrl+V re-open): `serve_attach` processes a whole `NetStreamData` batch of N operator `Input` records in its inner `for rec in decoder.push()` loop, calling `brain.send_effect(op_id, &bytes)` N times WITHOUT returning to `read_event()` — so the brain writes N `KIND_INPUT` frames back-to-back and drains nothing. The broker's single-threaded per-conn handler answers EACH with `send_frame(applied_envelope)` on the SAME conn (B5 exactly-once ack, KNOWN-HAZARDS 7.2). With the brain not reading, the broker→brain return direction fills (~10 frames = the IPC pipe buffer) → `send_frame` BLOCKS → the handler stops reading → the brain's writes block too → mutual full-duplex DEADLOCK. Capture pinned it: 11 input frames, write_input 11/11 (P0 holds — the PTY write is fine), ack send START=11 / END=10 (frame #11's applied-ack never returns). Same class as the v0.12.1 L0 two-conn split. Windows Terminal's ctrl+V paste accelerator was the trigger (injects the clipboard as a char-by-char key flood) but the deadlock is generic to ANY input flood, NOT ctrl+V-specific and NOT a P0 (PTY-write) or W1 (output-drain) regression. The applied-ack is load-bearing ONLY for `shellchan` (one-at-a-time spool delivery WAITS on `BrokerEvent::Applied`); `serve_attach` DISCARDS it (the operator/rc path is fire-and-forward, op_id for dedup only, never gates on the ack). FIX (doyle-approved): CONDITIONAL ACK — `InputReq` gains `ack: bool` (serde default = true, N-1-safe: an older brain's input still acks = today's behavior). `serve_attach`'s operator path calls `send_effect_no_ack` (ack=false) → `dispatch_input` writes NO applied frame → the per-conn handler never writes back while servicing the flood → it always drains → no deadlock (cures ANY input flood). `shellchan` keeps `send_effect` (ack=true) and its `Applied`-wait. Exactly-once PRESERVED: the broker still dedups by (session, op_id) at the applied-set regardless of the ack. N-1 caveat: an OLD resident broker (self-update window) ignores `ack=false` → still acks → the deadlock persists until a broker restart (inherent KNOWN-HAZARDS 7.9 broker-resident-wire-change class). (v0.13.0)
2026-09-06T13:09:01.8801675Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8801723Z 
2026-09-06T13:09:01.8801839Z ### REQ-RC-MOUSE-FORWARD
2026-09-06T13:09:01.8806208Z - Title: On Windows, `spt rc` must FORWARD scroll-wheel events to the harness when the harness has mouse reporting on. ROOT (operator HITL): P1's RawGuard EnableMouseCapture (added for right-click paste, REQ-RC-WIN-PASTE) makes Windows Terminal forward ALL mouse — including the scroll wheel — to rc instead of scrolling its own buffer, but the rc mouse handler dropped everything except right-button-down → scroll DIED (and WT's native scrollback is stolen by the capture). Operator ruling: keep mouse capture + right-click bracketed paste AND forward scroll to the harness. FIX (doyle design, cfg(windows), folds into the rc mouse handler): TRACK the harness's mouse-reporting mode by scanning its OUTPUT stream for the DECSET set/reset — ESC[?1000h/1002h/1003h (mouse on) + ESC[?1006h (SGR ext) and their ...l (off) — into a shared MouseMode{enabled,sgr} (pump writes from output, stdin reader reads); the scan survives a sequence SPLIT across output chunks (a bounded carry buffer). The mouse handler: right-button-DOWN -> bracketed clipboard paste (unchanged, REQ-RC-WIN-PASTE); ScrollUp/Down -> translate to an xterm SGR mouse report (ESC[<64;col+1;row+1M up / ESC[<65;..M down, 0-based crossterm -> 1-based xterm) and forward ONLY when enabled && sgr (else DROP — a legacy X10 report the harness may not parse is garbage); Moved/drag/left/middle -> DROP (scroll is the operator's need; click-forward risks garbage, no click-to-position). Unix UNCHANGED (no capture; the terminal scrolls natively). (v0.13.0)
2026-09-06T13:09:01.8806327Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8806494Z 
2026-09-06T13:09:01.8806604Z ### REQ-HAZARD-CONTROLLER-WRITER-REORDER
2026-09-06T13:09:01.8821700Z - Title: Two `controller_writer` threads must never race ONE brain↔broker connection's socket. ROOT (doyle, instrumented RACEDIAG repro on kitsubito): on a brain-restart re-serve the handoff brain registers as controller on the SAME session TWICE over the SAME `Brain::conn` socket — (1) `Brain::handoff` eagerly `subscribe(prior.session_id, prior.next_seq=1)` → `become_controller(from_seq=1)`, initial=[1], spawns writer-A (writes seq 1); (2) `serve_attach` re-handles the replayed `Request{from_seq:0}` → `attach_as(sid,0)` → `become_controller(from_seq=0)`, initial=[0,1], spawns writer-B (writes 0 then 1). `become_controller` (broker.rs) drops the prior `ControllerSink` (its `tx`) but does NOT stop the prior writer thread — writer-A keeps flushing its owned `initial` batch, and both writers hold clones of the same `SharedSend` (`Arc<Mutex<socket>>`) with NO inter-thread ordering. When writer-A's seq 1 wins the socket before writer-B's seq 0, the strict legacy consumer (brain.rs read_event reject-gap path) sees `output gap: got seq 1 want 0` → the test `attach_survives_target_brain_restart_exactly_once` panics at `.expect("re-serve")` OR HANGS in `render_until` (serve thread died on the gap → MARKER_TWO never reaches the wire). `prior.next_seq` is life1's CONSUMPTION cursor, NOT life2's connection state — life2's socket has been sent NOTHING, so a `from_seq=0` full replay on a connection that already streamed seq 1 is contradictory. Snap-above tolerance ALONE can't fix it (it would dedup-drop the late seq 0 → byte loss → the exactly-once byte-identity assert fails). PRE-EXISTING, surfaced by the v0.13.0 green-both-runners release gate; P1b is INNOCENT (its diff touches only input-ack machinery, proven mechanically + the test passes post-P1b in isolation). Sibling flaky cluster: `inject_control_wedge::g2`, `broker::spawn_env_reaches_child`. INVARIANT: on a single brain↔broker connection the controller output-frame stream is monotonic non-decreasing in seq (modulo dedup re-sends); exactly ONE `controller_writer` is ever live per connection; a SUPERSEDED writer writes NO further frames; a re-serve never replays a seq below what the connection already received. FIX (doyle design, corrected at the gate 2026-06-20): fix #1 as designed ('drop handoff's eager subscribe so serve_attach's attach_as is the sole registration') was REVERTED — handoff's `subscribe(prior.next_seq)` IS the standalone-resume mechanism (the brain-only update engine `apply_brain_only` + the `handoff`/`idempotent`/`daemon_e2e` int tests replay output through it with NO `serve_attach`; dropping it hung every resume-via-handoff test). The shipped fix is three parts: (1) CORRECTNESS — `Brain::handoff` seeds `session_cursors` at `prior.next_seq` so the consumer runs the production dedup-below+snap-above path, never the strict reject-gap legacy trap; this is COMPLETE (not merely tolerant) because every `controller_writer` emits an ASCENDING seq stream and the surviving writer (serve_attach's attach_as(sid,0)) offers the complete `[0,end]` range, so a snap-above merge of ascending writers delivers `[K,end]` with no skip/dup (first sighting of any seq>M is preceded by M on that writer). (2) INVARIANT — `controller_writer`'s INITIAL-BATCH replay is epoch-gated: `controller_epoch` is a shared `Arc<AtomicU64>`, the writer re-reads it UNDER `send.lock()` (atomically with `write_frame`) and returns the instant it is superseded — no check-then-block-then-write window, no superseded replay (W1-safe: never blocks the drain under `Mutex<OutputLog>`). The LIVE loop is NOT gated (new output only flows to the current controller; a superseded writer must still deliver its terminal `Displaced` kick — gating it suppressed the loud-take notice; it ends on `tx`-drop). (3) EXPLICIT-RESUME / OPERATOR-STREAM BOUNDARY (the LOAD-BEARING fix — kitsubito RACEDIAG ~33% repro the keystones missed) — `Brain::subscribe_with` (shared by attach/attach_as) resets the resume-mode dedup cursor to `from_seq`. The handoff eager subscribe makes serve_attach's brain receive the replay's seq K BEFORE the operator Request is processed (`attached`=false); that frame is dropped by the if-attached forward gate but the snap-above cursor already advanced past K, and `attach_as(sid,0)`'s re-subscribe used to leave the cursor advanced → the broker's re-send of seq K arrives below it, deduped, never forwarded → operator viewport forward-gap (silent content loss in the real rc consumer). Resetting to from_seq on the attach_as re-subscribe re-delivers from 0 (operator dedups the overlap) so seq K reaches the viewport. The epoch gate (2) is sound (RACEDIAG: zero socket interleaving above K); cold-start brains (empty map — production dispatch serve) keep the legacy next_seq path, so production is unaffected. (v0.13.0)
2026-09-06T13:09:01.8822173Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8822202Z 
2026-09-06T13:09:01.8822316Z ### REQ-HAZARD-CONTROLLER-RETAKE-FLOOR
2026-09-06T13:09:01.8826626Z - Title: `become_controller` should STRUCTURALLY refuse a controller re-take whose `from_seq` falls below the connection's already-delivered contiguous floor — making the P1c reorder invariant un-reintroducible by a future caller, not just removed at the one caller. ROOT/SCOPE (doyle proposed, P1c gate dialogue): P1c fixes REQ-HAZARD-CONTROLLER-WRITER-REORDER three ways (handoff single-take + epoch-gate-under-lock + session_cursors seed), removing the one decreasing-floor double-take and bounding any other to already-committed-only. A self-enforcing broker guard would refuse the bad SHAPE outright. BLOCKER: the obvious predicate (`from_seq >= delivered_through`) is UNSAFE because `delivered_through` is SESSION-WIDE (the `Arc<AtomicU64>` on `OutputLog`, shared by all controllers/viewers, advanced monotonic-MAX; `resume_seq` reads it) — a normal fresh-operator `from_seq=0` attach to a producing session legitimately sits below it (full ring replay + consumer dedup-below/snap-above), and monotonic-MAX can't distinguish the hazard (a `seq1`-without-`seq0` write reads as `2`). The structurally-correct guard needs a NEW per-connection contiguous-sent cursor (the true highest-contiguous seq this socket has received) that does not exist today; the guard then refuses a re-take below THAT. Bigger than P1c; no live gap (P1c fully fixes the actual bug). Mint/refine stages when the per-connection cursor is built. (v0.13.0 follow-up, post-ship)
2026-09-06T13:09:01.8826798Z - Required stages: 
2026-09-06T13:09:01.8826827Z 
2026-09-06T13:09:01.8826937Z ### REQ-ADAPTER-MULTIPLATFORM-SPT
2026-09-06T13:09:01.8829633Z - Title: A `.spt` adapter archive may pack multiple platforms in one signed asset: shared `manifest.toml` + `strings/` at the root, role binaries under per-Rust-target-triple subdirectories (ADR-0016 triple vocabulary, e.g. `x86_64-pc-windows-msvc/`); install/update extracts the shared root plus ONLY `current_platform()`'s triple subdir, flattened into `install_dir` so flat `<install_dir>/<program>` resolution (REQ-INSTALL-11) is unchanged. Name stays `adapter.spt` (plain-tar or gzip, `--asset` optional default); one whole-archive Ed25519 signature over the fat archive (REQ-UPD-9 single-artifact verify). A legacy flat archive (no triple subdirs) extracts as today (free back-compat); a multi-platform archive sets `min_spt_core_version >= 0.13.2` (forward-compat gate, readable before extract); a multi-platform archive missing the recipient's triple -> typed `NoArtifactForPlatform`, never a silent no-op. Large adapters may still split per-platform (single-triple archives via `--asset`, or ADR-0016 update-set machinery). (ADR-0024, v0.13.2)
2026-09-06T13:09:01.8834300Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8834338Z 
2026-09-06T13:09:01.8834501Z ### REQ-ADAPTER-LIVE-UPDATE
2026-09-06T13:09:01.8837302Z - Title: An adapter update is live and daemon-coordinated (the adapter analog of brain self-update, ADR-0004): for an endpoint with a running RESIDENT adapter binary (today the `[message-idle-translation-binary]`), the CLI keeps fetch+verify and hands the APPLY to the daemon over IPC, which per affected endpoint (1) STOPS the resident binary -> releases the OS file lock (fixes the Windows 'Access denied (os error 5)' overwrite failure), (2) swaps on disk ONLY files whose CRC differs from the staged archive (unchanged files + their still-running binaries untouched), (3) RE-CLONES the new on-disk manifest into the running `BrainLifecycle` (the in-memory manifest is cached at bringup and otherwise goes stale -> binaries+manifest back on the same page), (4) RESTARTS the resident binary from the new files. An endpoint NOT running -> CLI swaps directly (no lock, no cache). Only the resident class is cycled; ephemeral adapter binaries (Psyche loop, `[digest]` extractor, `[session.*]` runners, hooks) self-heal on next spawn and are excluded. The daemon keeps a per-endpoint registry of resident adapter children. (ADR-0025, v0.13.2)
2026-09-06T13:09:01.8837593Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8837636Z 
2026-09-06T13:09:01.8837739Z ### REQ-HAZARD-ADAPTER-APPLY-SILENT-NOOP
2026-09-06T13:09:01.8842411Z - Title: A DELEGATED live adapter apply MUST NEVER report success without performing the swap, and the live-update seam MUST use ONE parent-aware adapter matcher across all its comparators. TWO defects made the field repro (BUILD-F015B-APPLYMATCH: `--adapter cc:ccs` live update silently no-ops): (D1, matcher skew) the broker's dispatch_adapter_apply filtered sessions by EXACT `s.adapter == req.adapter`, but a `--adapter <adapter>:<profile>` endpoint stores the COMPOSITE `cc:ccs` while the apply carries the PARENT record name `cc` — so every :profile endpoint fell out to affected=[]; select_endpoints_running_adapter had the same `adp == adapter` skew, while the CLI live-gate (adapter_has_live_endpoint) already parent-matched — divergent rules on ONE seam. (D2, silent success) the affected.is_empty() branch replied KIND_APPLIED and RETURNED WITHOUT SWAPPING; once the CLI delegates the apply there is no CLI-side fallback swap, so success-without-swap = the update never lands (re-register re-reads the OLD manifest, version-of-truth honestly says old). FIX: (1) ONE shared spt_runtime::profile::adapter_parent_matches(session_adapter, parent) used by the live-gate + broker apply-filter + select_endpoints_running_adapter (no exact `==` against a record name at any live-update seam); (2) the daemon owns the whole apply once delegated — the CRC swap runs UNCONDITIONALLY (terminate/restart loops no-op when nothing is resident), KIND_APPLIED reported ONLY after a real swap. (F015B, ADR-0025 amendment)
2026-09-06T13:09:01.8842601Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8842630Z 
2026-09-06T13:09:01.8842760Z ### REQ-HAZARD-STOP-PATH-PSYCHE-ORPHAN-REAP
2026-09-06T13:09:01.8845634Z - Title: Endpoint-stop and brain-death reconcile MUST reap a brain-less perch's orphan detached Psyche via the cmdline-scoped guard (`psyche_orphan_should_reap`) — the handle-reap (`LiveSet::stop_host`, REQ-HAZARD-UNHOST-PSYCHE-REAP) CANNOT, because the owning brain is gone (its `psyche_child` handle died with it), and the brain-start scoped-reap (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP) never fires for a perch being STOPPED rather than re-hosted. So the live-host calls the scoped reap after `stop_host` at the reconcile stop-side AND in `confirm_residency_or_unhost`. Preserves fail-safe-decline (pid-alive AND exe-basename==psyche-program AND cmdline contains `<id>-psyche`; any unreadable signal DECLINES — a missed dup is bounded, a wrong-kill is catastrophic). This is the orphan-leak half of the perri F-010xF-015 field bug (the unsupervised install-dir Psyche that locked an update); the other half is the psyche own-copy (ADR-0025 amendment). (v0.13.2 W3 (a))
2026-09-06T13:09:01.8845747Z - Required stages: 
2026-09-06T13:09:01.8845776Z 
2026-09-06T13:09:01.8845886Z ### REQ-ADAPTER-UPDATE-MESSAGE
2026-09-06T13:09:01.8847413Z - Title: An adapter manifest may declare `[update].message` — a plain (multi-line) human notice surfaced to stdout, markdown-rendered (the v0.13.0 helpfmt prose path), ONLY when `spt adapter update` actually APPLIES an update (version changed), not on a no-op. Read from the newly-installed manifest; avenue-agnostic (gh_release/delegated/file_pull). No `{key}` substitution. Use: an adapter telling the operator a post-update action, e.g. spt-claude-code's "run `/reload-plugins` in any ongoing sessions". (v0.13.2)
2026-09-06T13:09:01.8847675Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8847704Z 
2026-09-06T13:09:01.8847809Z ### REQ-ADAPTER-GH-TRANSPORT
2026-09-06T13:09:01.8849359Z - Title: The `gh_release` avenue (and `spt adapter add --release`) gain a fetch `transport`: `https` (current reqwest direct, public), `gh` (shell the pre-authorized `gh` CLI — the private-repo path; `gh` honors OAuth and `GH_TOKEN`, so spt custodies no token), or `auto` (default: prefer `gh` when installed+authed, else HTTPS). `--gh`/`--https` force it on `add`. Additive over the existing fetch path; verify->extract->register downstream is unchanged. (v0.13.2)
2026-09-06T13:09:01.8849479Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8849508Z 
2026-09-06T13:09:01.8849613Z ### REQ-ADAPTER-PROOF-DIR-OVERRIDE
2026-09-06T13:09:01.8851044Z - Title: The author-time proof commands (`spt adapter digest-proof`, `spt adapter translate-proof`) gain a `--dir <path>` / `--manifest <file>` override so an author proofs a DEV binary against an on-disk manifest+install dir WITHOUT staging a full extracted GhReleaseManaged install (mirrors digest-proof's `--sample` pointing straight at a file). Fixes perri F-011: a bare-file-added gh_release adapter currently can't be resolved by the *-proof commands ('manifest is not present yet at <dir>'); un-stales the bare-file digest-proof int. (perri F-011, v0.13.x DX)
2026-09-06T13:09:01.8851219Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8851248Z 
2026-09-06T13:09:01.8851354Z ### REQ-ADAPTER-VERSION-CMD
2026-09-06T13:09:01.8852894Z - Title: `spt adapter version <name>` prints a registered adapter's declared version — the EXISTING mandatory `[adapter].version` manifest field (manifest.rs already requires it; NOT a `[strings].version`, NOT `get-string`, no second version source). A new `AdapterCmd::Version{option}` resolves the option's merged view via `registry::resolve_option` like the sibling adapter subcommands and prints `manifest.adapter.version`; an unresolvable option errors (exit 1) the same way. (v0.13.2 W6)
2026-09-06T13:09:01.8853010Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8853038Z 
2026-09-06T13:09:01.8853138Z ### REQ-DOCS-NO-INTERNAL-CODES
2026-09-06T13:09:01.8854757Z - Title: Public CLI --help (the clap `///` doc-comments) and the generated `docs-site/src/cli/reference.md` MUST NOT contain internal tracker/decision codes — `REQ-*`, `F-###`, `M#-W#`, `ADR-####`. They are meaningless to an end user reading --help and ship to GH-Pages. A CI-gated scan (the `xtask check` docs gate) fails on any such token in the GENERATED reference.md (which by construction contains only clap help, so rustdoc `///` on fns/structs is OUT of scope and keeps its REQ/ADR cross-refs). Substance is kept; codes are rewritten to plain language. (v0.13.2 W6)
2026-09-06T13:09:01.8854868Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8854896Z 
2026-09-06T13:09:01.8854997Z ### REQ-RUN-MULTISUBNET-HOME
2026-09-06T13:09:01.8857706Z - Title: `spt endpoint run` resolves the home subnet at the skeleton-create step and pre-creates the skeleton perch carrying it, so the harness `bind` inherits home via establish_perch's immutable prior-branch (no hook change, no env injection). Resolution: sole-subnet auto; multi-subnet + no --subnet + NON-interactive terminal -> refuse early with MRU-ordered --subnet guidance (never the silent 25s online-timeout); multi-subnet + no --subnet + INTERACTIVE -> print proposed config (id/project/adapter[:profile]/home=MRU-default) + 'Ok to proceed? Y/n', n -> --subnet guidance; --subnet overrides + validates membership. MRU = ordered move-to-front LISTs at two levels (per-project + always-updated node-global fallback). Home stays IMMUTABLE (ADR-0010). Fixes the LATENT multi-subnet bringup gap (perri, not a regression — HOME_REFUSED established >=0.11.0; exposed by the node crossing 1->2 subnets). (ADR-0026)
2026-09-06T13:09:01.8857926Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8857953Z 
2026-09-06T13:09:01.8858065Z ### REQ-ENDPOINT-UNBOUND-ATTACH
2026-09-06T13:09:01.8860750Z - Title: An spt-hosted endpoint is ATTACHABLE between spawn and bind: gate the attach on the broker SESSION being attachable (session+PTY+OutputLog exist at spawn, before bind), not on perch STATUS_ONLINE (bind). cmd_endpoint_run + `spt rc <id>` attach to a live broker session regardless of perch status (headless bringups too; lets an operator clear a bind-gating prompt) -- replaces await_endpoint_online; preserves REQ-HAZARD-RC-ATTACH-ONLINE-RACE's 'no attach before a session' intent at the earlier session-exists point; source = the broker sessions map (ADR-0025 W3a); local-only. New on-disk status STATUS_UNBOUND (spawn->unbound, bind->online, death->offline); lifecycle reuses the existing exit-waiter/reconcile (session death->offline); unbound is attachable but NOT message-addressable (messaging stays online/bound-gated). EpDisplay gains Unbound = HOLLOW (+ hollow-controlled variant) -- amber=HarnessOnly is taken + means not-controllable (the opposite of attachable). (ADR-0027)
2026-09-06T13:09:01.8860922Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8860950Z 
2026-09-06T13:09:01.8861045Z ### REQ-MANIFEST-SUBST
2026-09-06T13:09:01.8863741Z - Title: Manifest substitution primitives for resolve-not-execute (ADR-0029, supersedes a rejected `spt api run-hook`): (1) two adapter-static substitution keys `{adapter_dir}` (the registry record's precise source_dir — install dir, survives updates, the dir bare-program resolution uses) and `{adapter_name}`, available wherever command/string substitution runs; (2) lazy substitution INSIDE `[strings]` values at `get-string` read time, scoped to those adapter-static keys ONLY (session-scoped {id}/{session_id}/… are NOT available — get-string carries no session; a get-string --session-id is a deferred larger change). Invariant preserved: spt-core never executes a string — it substitutes and returns; the adapter's own wrapper executes the result (e.g. a CC hook dispatcher get-strings its packed binary once per session into an env var, then runs it per-hook, so hook logic rides `spt adapter update`). (v0.16.0)
2026-09-06T13:09:01.8863861Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8863894Z 
2026-09-06T13:09:01.8863989Z ### REQ-ADAPTER-UPDATE-POST
2026-09-06T13:09:01.8866997Z - Title: Composite adapter update — an avenue-agnostic `[update.post]` sub-table `{ command, self_verifies }` run AFTER the primary avenue (gh_release/file_pull/delegated) resolves, in the same `spt adapter update` (ADR-0029). Runs UNCONDITIONALLY (even on an adapter version no-op — the post-step's own idempotent check decides). PUBLISHED stdin JSON seam: one line `{adapter_applied, adapter_name, profile_name, version, previous_version, adapter_dir}` (additive keys; post-step ignores unknown). stdout decides the notice: custom text SUPERSEDES [update].message; a reserved sentinel fires the static [update].message; empty = no notice. exit code orthogonal (0 ok / nonzero failed). Precedence: dynamic-stdout > sentinel/manifest-message > nothing. NO [update.post] declared ⇒ today's adapter_applied→[update].message unchanged; post-step FAILS ⇒ loud warning + fall back to adapter_applied→message. FAILURE-ISOLATED: a committed gh_release pull is never rolled back if the post-step fails (independent channels). (v0.16.0)
2026-09-06T13:09:01.8867116Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8867144Z 
2026-09-06T13:09:01.8867239Z ### REQ-TRANSLATE-COMMAND
2026-09-06T13:09:01.8870672Z - Title: `[message-idle-translation-binary]` accepts a `command` (opaque; args + ADAPTER-STATIC {adapter_dir}/{adapter_name} substitution ONLY — ratified v0.16.0 W1, NOT session {key}: the translation binary is a persistent process serving all sessions on the endpoint (session/event ctx arrives per-message via the stdin Init/Event protocol, never the spawn argv) and the live-update respawn site has no session ctx (a {id}-bearing command would MissingKey→spool); program token resolved against install_dir like [digest].extractor/[session.psyche_init]) in addition to the bare `path`. `path` is DEPRECATED — keeps parsing (manifest forward/back-compat) but emits a registration warning steering to command. Exactly one of {path, command} (both-set refused at registration; neither = no translation binary). The spawn lifecycle + stdin/stdout JSON-lines protocol (Init/Event/Input → key/text/delay_ms/commit) are UNCHANGED — command alters only how the executable+args are located/launched (read_translation_path → read_translation_command). Unblocks folding `claude-spt translate` into the one consolidated binary (downstream ADR-0006). (v0.16.0)
2026-09-06T13:09:01.8870957Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8870991Z 
2026-09-06T13:09:01.8871095Z ### REQ-SEND-REPLYTO-REMOVE
2026-09-06T13:09:01.8872469Z - Title: Remove `--reply-to` from `spt send` — a target-fallback + REPLIED-label nicety that confuses agents, with no wire effect (ADR-0020 already made messages structural (from,body), no __REPLY_TO__). Hard-remove (no deprecation shim): the clap flag, the is_reply/REPLIED label branch (always SENT/QUEUED), the `send` how-to --reply-to example, and the reply-to mention in REQ-DOCS-6's send topic. Reply-correlation stays on the structural `from` attribute. (v0.16.0)
2026-09-06T13:09:01.8872630Z - Required stages: impl, unit
2026-09-06T13:09:01.8872658Z 
2026-09-06T13:09:01.8872748Z ### REQ-DIGEST-CURSOR
2026-09-06T13:09:01.8876332Z - Title: `spt endpoint digest` gains incremental turn-end consumption (extends REQ-TERM-4/5): `--last <N>` = the last N TURNS (the digest's natural unit; --last 1 = the latest turn = turn-end output); a per-entry STABLE SOURCE-DERIVED `seq` in the --json output (deterministic from the entry's append position in the source — transcript record index across the session ledger / digest.log index — so a live re-projection yields the same seq for the same committed entry; NOT a window-position index that renumbers on slide); `--after <seq>` = entries newer than seq still in the window (full window + signal if seq predates it, mirroring the version-slide full-refresh). An in-flight (still-growing) entry is flagged `partial: true` with NO stable seq until finalized (consumer reprocesses partial, skips <= seq). Also emit per-entry `ts` where present (seq is the authoritative dedup+cursor key). The digest's agent text is sufficient fidelity (no raw-source mode). BINDING doc-guidance: an adapter's [digest] extractor / api digest-entry MUST classify delivered user-facing messages as turn-opening `input` (equiv to direct PTY user-input), else messaging-driven sessions collapse into a few giant turns and --last/seq lose granularity. (v0.16.0)
2026-09-06T13:09:01.8876461Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8876489Z 
2026-09-06T13:09:01.8876579Z ### REQ-CLI-JSON
2026-09-06T13:09:01.8878608Z - Title: A global `--json` flag (clap global=true) honored by the READ/STATUS command set: endpoint list/whoami, daemon status, subnet status/show-code, endpoint description/role, adapter list/version, notif list, grant list, access list, shell list, how-to (endpoint digest already has it). Action commands do not honor it. A shared print_json() helper + a coverage TEST asserting every command in the set emits valid JSON (guards against the missing-shared-formatter drift). Output uses explicit per-command output DTOs with committed field names — internal structs are NOT serialized verbatim (their fields would become a public contract; JSON is a consumed wire-parity surface). (v0.16.0)
2026-09-06T13:09:01.8878717Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8878745Z 
2026-09-06T13:09:01.8878840Z ### REQ-RUN-EMPTY-CREATE
2026-09-06T13:09:01.8880234Z - Title: `spt endpoint run` / bare `spt` routes a TOTALLY-EMPTY scope straight to the endpoint-creation flow: when gather_endpoints() is empty (nothing attachable, local OR subnet) PickerModel::new opens on Screen::CreateAdapter instead of PickExisting (today it always opens PickExisting + renders a blank list). A node WITH subnet endpoints but no local ones still has things to pick → stays on the picker. Extends REQ-RUN-PICKER. (v0.16.0)
2026-09-06T13:09:01.8880487Z - Required stages: impl, unit
2026-09-06T13:09:01.8880516Z 
2026-09-06T13:09:01.8880610Z ### REQ-RC-IDENTITY
2026-09-06T13:09:01.8882900Z - Title: `spt rc` overlays a persistent endpoint-identity marker so the operator always groks which endpoint they control: a reserved TOP status row via a DECSTBM scroll-region (shrink the PTY's reported rows by 1, own the row), right-aligned `SUBNET : ENDPOINT_ID @ NODE`, CYAN text. Re-assert the margin + repaint on alt-screen enter / DECSTBM reset / resize (output-scanning, like the existing mouse_scanner). NO window title (the harness, e.g. CC, owns it for busyness — OSC dropped). Resolve subnet/node/id once at attach (perch/registry read) and thread into the pump; subnet = the endpoint's home/primary ("local" if none). The literal floating rounded-rectangle corner box is DEFERRED to the future web-based GUI (not a grid-model rc — that lift is better spent on the GUI). (v0.16.0)
2026-09-06T13:09:01.8883021Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8883101Z 
2026-09-06T13:09:01.8883195Z ### REQ-JOIN-TWO-PHASE
2026-09-06T13:09:01.8886682Z - Title: `spt subnet join` is two-phase (meet-before-code) so the entered code is FRESH at the ceremony regardless of discovery time (ADR-0030). The meet selector is the PUBLIC `(subnet-name, TOTP-epoch)` (rendezvous_token = SHA256(domain ‖ name ‖ totp_step)); the secret TOTP-code is the SPAKE2 password ONLY, never a discovery input — so the code is collected AFTER a member is found. Extend the brain.pair_join event stream (brain.rs:1009): CLI PairMeetReq{subnet} → daemon meets (name, current-epoch) resolving the seed-holder's REAL stable pairing address → MetMember event → CLI prompts the code (cli.rs cmd_subnet_join :6236) → PairCodeSubmit{code} → daemon dials the held real-address on SPT_PAIR_ALPN + SPAKE2 → PairJoined/PairFail. Daemon holds the real-address between phases, bounded by a 5-MINUTE wait-for-code timeout; a wrong-code retry re-runs the CEREMONY ONLY against the held address (no re-search). The non-interactive `--code` path stays one-shot (no prompt; relies on REQ-NET-FAMILY-GATE fast discovery, fails loudly per REQ-JOIN-DIAGNOSTICS on staleness). Security unchanged — the meet is pre-trust/unauthenticated (SPT_PAIR_MEET_ALPN); auth stays in SPAKE2. (next milestone)
2026-09-06T13:09:01.8886824Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8886853Z 
2026-09-06T13:09:01.8886944Z ### REQ-NET-FAMILY-GATE
2026-09-06T13:09:01.8889511Z - Title: spt-core binds only IP families that are actually REACHABLE (ADR-0030), so a half-broken family (e.g. IPv6 whose AAAA resolves but whose path is dead) never silently consumes connection/discovery time. At NetEndpoint::bind (endpoint.rs), probe each family's reachability ONCE and bind only the working ones: dual-stack when both healthy; IPv4-only when IPv6 is dead; IPv6-only when IPv4 is dead (drop the DEAD family — NOT a fixed prefer-IPv4; IPv6-only networks must keep working). Re-evaluated on daemon restart (once-at-bind, no live re-eval in v1). Explicit overrides SPT_DISABLE_IPV6 / SPT_DISABLE_IPV4 force a family off (escape hatch + determinism + testing, mirroring SPT_NTP_SERVER); a forced-off family is never bound regardless of the probe. Reusable beyond join — every spt connection benefits. (next milestone)
2026-09-06T13:09:01.8889639Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8889667Z 
2026-09-06T13:09:01.8889760Z ### REQ-JOIN-DIAGNOSTICS
2026-09-06T13:09:01.8892558Z - Title: `spt subnet join` never fails SILENTLY (ADR-0030; the field incident showed no output at all). (a) LIVE progress during the meet (replace the one-shot "Searching…" cli.rs:6268 with periodic elapsed/deadline) so silence ≠ hang; (b) DETAILED failure on meet-exhaustion — rendezvous candidates + families attempted (IPv4/IPv6) + relay-vs-direct + the last concrete error — surfaced BEFORE any code prompt (a dead subnet must not make the user fetch a code); connect_seed_holder (pairhost.rs:437) and dial_via_rendezvous (meet.rs:281) currently swallow per-attempt errors — thread the last error up with attempt context; (c) PROPAGATE the terminal event — brain.rs:1024 `_ => continue` must deliver a daemon NoSeedHolder/PairFail to the CLI as a printed error (this is WHY the user saw nothing); (d) `--verbose`/`SPT_LOG` discovery TRACE (per-probe derived id, discovery path mDNS/n0-DNS/relay, per-family timeouts), opt-in — no such knob exists today. (next milestone)
2026-09-06T13:09:01.8892831Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8892859Z 
2026-09-06T13:09:01.8892961Z ### REQ-PRESENCE-LIVENESS-TRUTH
2026-09-06T13:09:01.8897801Z - Title: A node's gossiped per-endpoint registry Status reflects real liveness, so a remote viewer never paints a DEAD endpoint as ONLINE. ROOT (confirmed + gated vs CONTEXT.md): registryhost.rs:397-405 advertises a NOT-alive perch as Status::Dormant (the `else` of is_perch_alive), re-stamped every gossip round (never ages to Offline). Design intent GATED vs resting.rs + CONTEXT.md: active/dormant is the MULTI-INSTANCE routing differentiator (active = the bare-id routing target; dormant = a WARM non-target sibling — 'driving ling@laptop makes ling@desktop dormant', resting.rs:97; transitions active→dormant on AttentionShift/Detach). suspended = COLD (session closed, resumable-on-wake) while its NODE is UP. offline = NODE DOWN — NEVER self-gossiped (RestState has no Offline; a live node only ever gossips active/dormant/suspended), remote-inferred via epoch-lease eviction. So labeling a NOT-running perch Dormant is the DEFECT (dormant requires warm/running). PRIMARY FIX (registryhost `else`, not-bound-alive): live-but-UNBOUND (has a live broker session; is_perch_alive is bound-gated) → Active/Dormant (still warm); else (cold, no live session, but its node is up because this very daemon is gossiping) → SUSPENDED — NOT Dormant, NOT Offline (the node is UP; Offline is never self-gossiped). This alone removes the false-ONLINE. dormant keeps gossiping (routing/MRA needs it) but RENDERS as its online flavor (no distinct glyph; the dormant→suspended auto-suspend timer disambiguates recency). The DISPLAY of these states (incl Suspended=gray-filled) is REQ-SUBNET-DISPLAY-PARITY. Design: docs/design/subnet-presence-display.md §A. (next milestone)
2026-09-06T13:09:01.8897990Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8898018Z 
2026-09-06T13:09:01.8898130Z ### REQ-SUBNET-DISPLAY-PARITY
2026-09-06T13:09:01.8902547Z - Title: The `spt endpoint run` picker renders endpoint state IDENTICALLY for local and remote (subnet) rows — bound/unbound, controlled (+driver node), and harness-only are all visible across the subnet, not local-only. Today data.rs:293-294 reduces a remote row to plain green-filled/gray-hollow because those facts aren't propagated. (1) GOSSIP additive per-Instance fields: bound/unbound, controlled + driver-node, harness_only (Offline is never gossiped — node-down is remote-inferred). (2) Derive the full EpDisplay for subnet rows from the gossiped fields, same path as local (remove the remote-reduction). (3) PALETTE rework (fill = ACTIONABLE: filled=can act now (rc-control if online, WAKE if suspended-on-live-node) / hollow=cannot (no control seat / node gone)): green-filled=online+bound+free; blue-filled=online+controlled (desc shows `controlled by <node>`); RED-filled=online+UNBOUND (controlled-or-not; controlled-ness shown via available options not glyph) — replaces green-hollow Unbound + absorbs the dropped UnboundControlled; AMBER-HOLLOW=online+harness-only (no broker seat → can't rc) — was amber-FILLED; GRAY-FILLED=Suspended (cold, node up — wakeable) NEW; gray-hollow=Offline (node down) now REMOTE-ONLY. EpDisplay: drop UnboundControlled, Unbound→red-filled, HarnessOnly→amber-hollow, add Suspended(gray-filled). Picker maps Active|Dormant→online flavor, Suspended→gray-filled, Offline→gray-hollow. (next milestone)
2026-09-06T13:09:01.8902794Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8902823Z 
2026-09-06T13:09:01.8902924Z ### REQ-UPDATE-FETCH-CURRENT-UX
2026-09-06T13:09:01.8905777Z - Title: `spt update fetch` reports an already-staged / already-applied latest as an ACTIONABLE human outcome (exit 0), not a Debug-formatted error. ROOT: cmd_update_fetch (cli.rs) sets the rollback floor = staged_version, so when the published candidate == the already-staged version, verify_update_set_metadata returns Err(RejectReason::Rollback{current,candidate}) — printed as {reason:?} (Debug) at exit 1, reading as a FAILURE when the update is merely already downloaded and just needs `spt update apply` (this bit the operator: fetch kept 'failing', apply was the missing step). FIX: a PURE classifier (reason, applied, staged) -> {AlreadyStaged (latest downloaded, not yet installed) / AlreadyApplied (up to date) / GenuineError}; already-staged + already-applied print a friendly message and exit 0; genuine rejects use RejectReason's Display (release.rs, not Debug) + exit 1 — applied at ALL THREE fetch reject sites (metadata + artifact-verify + plan-verify). (v0.18.0)
2026-09-06T13:09:01.8905895Z - Required stages: impl, unit
2026-09-06T13:09:01.8905971Z 
2026-09-06T13:09:01.8906078Z ### REQ-UPDATE-FETCH-APPLY-FLAG
2026-09-06T13:09:01.8908642Z - Title: `spt update fetch --apply` is the one-shot get-to-latest: fetch, then INSTALL the staged update REGARDLESS of whether the fetch itself staged anything new — so the brittle `fetch && apply` chain (which broke when fetch no-oped / exited nonzero on an already-staged latest, skipping the chained apply) is unnecessary. Composes with REQ-UPDATE-FETCH-CURRENT-UX: the end state is 'installed latest', reached idempotently from new-staged -> apply / already-staged (applied<candidate) -> STILL apply / already-applied -> noop+exit0 / genuine error (bad signature, no artifact for platform, true downgrade, network) -> do NOT apply, propagate the error + nonzero. Reuses the existing cmd_update_apply core (its own verify + two-phase + auto-rollback own correctness; no duplicated swap/respawn). Additive clap flag (plain doc-comment, no internal codes); reference.md regenerated. (v0.18.0)
2026-09-06T13:09:01.8908750Z - Required stages: impl, unit
2026-09-06T13:09:01.8908779Z 
2026-09-06T13:09:01.8908884Z ### REQ-UPDATE-RUNNING-IMAGE-SURFACE
2026-09-06T13:09:01.8915616Z - Title: `spt` surfaces the RUNNING broker image version beside the on-disk version so an updated-looking node reveals broker-side dormancy. ROOT (F-025): `spt update apply` restarts the BRAIN only (ADR-0018 D3-3) — the BROKER process survives and keeps running its pre-apply compiled image, so every broker-side surface of a freshly-applied release (the F015B live-apply matcher, dispatch inject legs, etc.) is silently dormant until a full daemon bounce, with nothing in the CLI revealing the split. FIX: the running broker SELF-REPORTS its compiled image version over IPC (a new request KIND answered by the live broker process from its own compiled build constant) — HARD CONSTRAINT: the version comes FROM the running broker process, NEVER inferred from disk bytes, install manifest, or file timestamps, since the disk is exactly the half that is already ahead; a version-surface command (`spt version` and/or `spt daemon status`) prints the running-broker version beside the on-disk/product version and flags a mismatch. Keeps read-side truth independent of write-side claims (the field lesson from F015B). (F-025) AMENDED 2026-07-27 (FIELD-TRUTH W1 roll-in, operator-directed): (1) `spt daemon status` MUST also divulge the COORDINATOR (brain) image version — the process that executes daemon-coordinated features and the most relevant running version to a status reader; same HARD CONSTRAINT: sourced from the RUNNING brain process (self-report over the broker↔brain channel or equivalent live query), never inferred from disk. (2) The stale-daemon warning RE-KEYS to the coordinator image vs installed: it renders ONLY when the COORDINATOR is outdated — post-apply the brain restarts onto the new bytes while the broker legitimately stays old (ADR-0018 D3-3), so the broker-keyed warning fired exactly backwards on a healthy freshly-updated node. (3) The remedy line recommends solely `spt daemon refresh` (in-place coordinator cycle; hosted terminals and the net layer keep running), never `daemon stop` + `daemon start`; a broker-breaking release still forces its full bounce through the update machinery, not through status advice. (4) The broker image line stays, informational only — no warning, no remedy text. Coordinator-version query unanswered (older daemon) renders not-reported without the warning.
2026-09-06T13:09:01.8916018Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8916046Z 
2026-09-06T13:09:01.8916156Z ### REQ-UPDATE-APPLY-RESTART-NOTICE
2026-09-06T13:09:01.8918128Z - Title: `spt update apply` prints a LOUD restart-required notice whenever the surviving broker will keep running the pre-apply image (which, until broker-restart choreography exists, is ALWAYS on a successful apply). Public wording, no internal CODE:RESULT markers (composes with REQ-ADAPTER-UPDATE-MESSAGE / the update-apply-confident-message rule) — name the user-visible CONSEQUENCE ('daemon-coordinated features run the previous version until the daemon restarts'), not the broker/brain internals. Composes with REQ-UPDATE-RUNNING-IMAGE-SURFACE (the notice tells the user what the version-surface will then show, and how to clear it). (F-025)
2026-09-06T13:09:01.8918287Z - Required stages: impl, unit
2026-09-06T13:09:01.8918315Z 
2026-09-06T13:09:01.8918428Z ### REQ-UPDATE-APPLY-ALREADY-APPLIED
2026-09-06T13:09:01.8920839Z - Title: `spt update apply` classifies an already-staged / already-applied state as a friendly exit-0 no-op instead of dying at the binary-aside rename with 'Access is denied (os error 5)'. ROOT (F-025): a second apply on an already-applied staged version reaches the two-phase binary-aside rename and fails os-error-5, reading as a hard failure when the machine is simply up to date. FIX: apply gains the same pure classifier `fetch` got in v0.18.0 (REQ-UPDATE-FETCH-CURRENT-UX) — already-applied → clear message + exit 0, and the flow MUST short-circuit BEFORE the binary-aside rename in that state; mirror the classifier at ALL apply reject/entry sites the way the fetch fix covered its three. Genuine errors (bad signature, wrong platform, true downgrade, network) still propagate nonzero. (F-025)
2026-09-06T13:09:01.8920944Z - Required stages: impl, unit
2026-09-06T13:09:01.8920971Z 
2026-09-06T13:09:01.8921076Z ### REQ-BROKER-ATTACH-JOURNAL-RESILIENT
2026-09-06T13:09:01.8923734Z - Title: A poisoned EffectJournal mutex or a sick NetHost runtime must NOT permanently brick all future attaches. Bug #16 (URGENT): a live spt-hosted endpoint (eel-a) attach fails with 'brain IPC read deadline elapsed' after a self-update brain-respawn — the broker survives the respawn and one journaled op (dispatch_net_stream_open journal.apply_once + loopback open_stream runtime.block_on nethost.rs:1060) enters a bad state, so every journaled attach silently kills its per-conn reply thread while non-journaled ops keep working. Fix: recover PoisonError via into_inner (effect.rs apply_once, replace the .expect panics) so one panic cannot brick all attaches; bound the loopback open_stream block_on (nethost.rs:1060) like the QUIC bounded_block_on so a sick runtime fails fast with an error frame not an opaque 10s deadline. Reinforces REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #16.
2026-09-06T13:09:01.8923839Z - Required stages: impl, unit
2026-09-06T13:09:01.8923862Z 
2026-09-06T13:09:01.8923963Z ### REQ-WAN-SEND-DELIVERY
2026-09-06T13:09:01.8926138Z - Title: Bug #9/#10: cross-node spt send reports SENT(WAN) but does not deliver, even on stable-IP pairs. Real root: spt send resolves the dial with id-only addr_for_node_hex (endpoint.rs:538) which forces a fresh iroh discovery round-trip every send, while the gossip pump uses cached direct addresses (dial_seeded/PeerAddrStore) so gossip stays green but send rides a marginal discovery path that cannot carry the fire-and-forget payload; the handshake completes so SENT(WAN) prints falsely. Fix: (1) route the WAN dial through the pump seeded-direct-address resolution (PeerAddrStore first, id-only fallback); (2) receiver writes its WanOutcome back so the sender confirms delivery under the QUIC deadline and only reports SENT on confirmed delivery, honest failure otherwise. Access-gate/perch/spool all verified correct (ruled out). See docs/NEXT-MILESTONE-BUG-TRIAGE.md #9-10.
2026-09-06T13:09:01.8926382Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8926415Z 
2026-09-06T13:09:01.8926515Z ### REQ-RC-CROSS-NODE-ATTACH
2026-09-06T13:09:01.8928484Z - Title: Bug #4: spt rc to a remote endpoint fails with 'no live session' though endpoint list shows it Active — rc.rs:1063 resolves only the LOCAL broker session table and always dials loopback, never consulting the registry or dialing the owning node (the cross-node attach transport exists in the broker; only the client leg is missing). Fix: on a local resolve miss, resolve the owning node from the registry (reuse resolve_across_visible), net_dial that node, and run a remote session-resolve + serve_attach round-trip (mirror the wansend resolve-dial-round-trip pattern). Shares the resolve-owning-node primitive with REQ-WAN-SEND-DELIVERY. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #4.
2026-09-06T13:09:01.8928651Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8928679Z 
2026-09-06T13:09:01.8928764Z ### REQ-RC-WIN-VT-OUTPUT
2026-09-06T13:09:01.8932377Z - Title: Bug #12: `spt rc` to an endpoint renders ANSI escapes LITERALLY (raw ←[K / color codes) on a Win10 conhost console, garbling the viewport — while `endpoint run --attach` in the SAME env renders fine and Win11 Windows Terminal is unaffected. Root (code-grounded, doyle): rc.rs RawGuard::enable calls only crossterm enable_raw_mode (INPUT raw mode) and NEVER enables ENABLE_VIRTUAL_TERMINAL_PROCESSING on the OUTPUT handle; Win10 conhost defaults VT-output OFF so escapes print literally, whereas the picker/alt-screen setup on the endpoint-run path enters crossterm's VT-enabling console setup first (leaving VT-output on) — so it is the rc-attach CLIENT PATH specifically, and --attach-works-same-env confirms (not refutes) the VT-out theory. Fix: in the rc attach path (RawGuard), on cfg(windows) + interactive console (mirror the windows_mouse_wanted guard so piped stdin/stdout keeps clean bytes for the e2e byte tests), SetConsoleMode STD_OUTPUT_HANDLE |= ENABLE_VIRTUAL_TERMINAL_PROCESSING|ENABLE_PROCESSED_OUTPUT, capture the prior mode, restore on Drop. cfg(windows)-only, client-side, independent of #4/#6. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #12.
2026-09-06T13:09:01.8932500Z - Required stages: impl, unit
2026-09-06T13:09:01.8932528Z 
2026-09-06T13:09:01.8932630Z ### REQ-GOSSIP-CONTROLLED-ANY
2026-09-06T13:09:01.8935772Z - Title: Bug #3: a locally-controlled endpoint gossips controller_node = None so remote viewers show it free to control. Root AS IT STOOD WHEN THIS WAS WRITTEN: driven_by was stamped Some(node) only for a REMOTE WAN attach (attach.rs:337), because the legacy LOCAL attach path passed by=None (broker.rs:1750) — so the datum a remote viewer needed was absent at source. NOTE (doyle ruling v3, 2026-07-19): the accompanying model claim — that a local controller must NOT latch driven_by — was FALSIFIED. driven_by names the CONTROLLING node, own node INCLUDED (CONTEXT.md:386), and the own-hex latch is truthful; the by=None behavior was a legacy-path artifact, not the design. KH 7.15 is about CLEARING the stamp when the session is gone, which is a different rule and still stands. The fix as landed is unaffected and stays correct: broker stamps a SEPARATE any-controller datum (true/Some(host) for a local OR remote controller) alongside stamp_driven_by, and advertise_local gossips Instance controller_node from it. node-refresh is NOT the fix (data is absent at source). See docs/NEXT-MILESTONE-BUG-TRIAGE.md #3.
2026-09-06T13:09:01.8935991Z - Required stages: impl, unit
2026-09-06T13:09:01.8936019Z 
2026-09-06T13:09:01.8936114Z ### REQ-SUBNET-COUNT-ROUTABLE
2026-09-06T13:09:01.8937824Z - Title: Bug #2: a remote node endpoint count drifts (0/2, 1/3) because node_status_rows (cli.rs:5314) increments the per-node total unconditionally, counting non-routable Offline ghost rows; purge is not a registry eviction (it gossips a one-shot Offline row that is immortal on remote viewers — eviction is per whole-node only). Fix: routable-only denominator (total += status.routable()) keeping a separate raw count for the all-Offline liveness branch; plus per-row Offline-TTL eviction so purged endpoints stop accumulating on remote snapshots. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #2.
2026-09-06T13:09:01.8937977Z - Required stages: impl, unit
2026-09-06T13:09:01.8938010Z 
2026-09-06T13:09:01.8938107Z ### REQ-BROKER-SCREEN-GRID
2026-09-06T13:09:01.8940756Z - Title: Bugs #6 + #12 + #7/#8-artifacts: the broker is a raw-byte pump with no screen model — OutputLog replays the raw ring from seq 0 into a fresh terminal on attach, so an alt-screen TUI (Claude Code) corrupts scrollback (#6) and rc-to-a-pre-running-endpoint garbles (#12 — rc and endpoint run --attach are the SAME client fn, so it is replay content not a client-VT bug). Fix: a server-side VT/grid/screen model (tmux/mosh-style) that maintains authoritative screen + alt/main + cursor and synthesizes a CLEAN current-screen repaint on attach instead of replaying mid-stream ring bytes. Also eliminates residual-cell artifacts on animate/scroll/resize (#7/#8). Operator NON-NEGOTIABLE: accurate PTY representation with zero artifacts. (win32 vterm in the report means this server-side emulator, not ConPTY which is already the backend.) See docs/NEXT-MILESTONE-BUG-TRIAGE.md #6/#12.
2026-09-06T13:09:01.8940985Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8941037Z 
2026-09-06T13:09:01.8941122Z ### REQ-RC-IDMARKER-DISABLE
2026-09-06T13:09:01.8942574Z - Title: Bugs #14 + #7/#8 (marker half): feature-flag the top-right StatusRow endpoint-id marker OFF (rc.rs:198-307). It is a one-shot absolutely-positioned paint that scrolls off-screen and is not re-stickied (#14), and its DECSC/clear/SGR injection splices into the harness in-flight drawing causing residual artifacts (#7/#8). Ship disabled next release (operator: save the concept for a future web SPT GUI); revisit as a proper per-frame sticky overlay only once REQ-BROKER-SCREEN-GRID provides the screen model. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #14.
2026-09-06T13:09:01.8942679Z - Required stages: impl, unit
2026-09-06T13:09:01.8942708Z 
2026-09-06T13:09:01.8942807Z ### REQ-ENDPOINT-LIST-PALETTE
2026-09-06T13:09:01.8945156Z - Title: Bugs #11 + #15 (display): spt endpoint list renders status as plain text while the picker turns the same ResourceRow into the W5 colored EpDisplay palette. Fix: extract one shared ResourceRow-to-EpDisplay builder + make the picker display enums/helpers public, and have endpoint list render the same colored status squares (via helpfmt stdout_color, not ratatui Span). This also fixes #15 — a lone warm detached instance renders as its online flavor (Dormant maps to online) instead of leaking the bare word Dormant through the text-only list (no resting.rs/CONTEXT model change; operator ruling display-only). Couples REQ-PICKER-NODE-GROUPING (both edit subnet_rows — sequence the shared-builder extraction first). See docs/NEXT-MILESTONE-BUG-TRIAGE.md #11/#15.
2026-09-06T13:09:01.8945270Z - Required stages: impl, unit
2026-09-06T13:09:01.8945298Z 
2026-09-06T13:09:01.8945398Z ### REQ-PICKER-NODE-GROUPING
2026-09-06T13:09:01.8946756Z - Title: Bug #13: the endpoint run Subnet tab shows a machine once PER shared subnet (subnet_rows data.rs:253 iterates per-subnet, groups by subnet:node, no cross-subnet dedup). Fix: dedup by (node, endpoint_id) across the subnet loop, collect the set of shared subnet names per endpoint, emit one group per MACHINE (group = node_display) with its shared subnets listed beneath the machine name; reconcile per-endpoint status across subnets (most-alive). Couples REQ-ENDPOINT-LIST-PALETTE (both edit subnet_rows). See docs/NEXT-MILESTONE-BUG-TRIAGE.md #13.
2026-09-06T13:09:01.8946967Z - Required stages: impl, int
2026-09-06T13:09:01.8947044Z 
2026-09-06T13:09:01.8947160Z ### REQ-ENDPOINT-LIST-NODE-GROUPED
2026-09-06T13:09:01.8952089Z - Title: spt endpoint list is node-grouped over unique INSTANCES, not subnet-grouped over duplicated rows: one row per (id,node) instance — subnet duplication collapsed (ADR-0006 §1: subnet is never an identity axis), freshest-epoch wins a cross-subnet status disagreement (the resolve_among twin rule) — grouped under 'This node: <label>' (cyan; local roster is the status truth, advertised-status vocabulary, corrupt = suspended + corrupt annotation) then remote nodes alphabetical (node name orange 38;5;208, the legacy $LIVE orange), this node carrying a light-gray 'Joined subnets:' line (the subnets this node is a member of -- renamed from 'Shared', releases#72, see REQ-ENDPOINT-LIST-JOINED-SUBNETS-LABEL) and each REMOTE node a light-gray 'Shared subnets:' line (subnets among our memberships where that node gossips any visible row; per-instance subnet detail stays in --json/--detail), instance rows [id, endpoint_type, glyph, status] (endpoint_type threaded from Instance.endpoint_type through ResourceRow — additive, pre-field rows render '-'), per-node 'Total:' lines with NO grand total (the stderr ENDPOINTS:<n> line is REMOVED — it counted subnet rows, so the same instance in N subnets counted N times), SELF pin kept first with a '(self @ <node>)' marker (REQ-WHOAMI-1 alias; self also appears as a This-node row so the node total stays honest), remote nodes with zero visible instances skipped, --subnet narrows the union to that subnet's view, --json DTO structure UNCHANGED (committed surface; gains only an additive endpoint_type field). Grill-with-docs ruling 2026-07-02 (operator + doyle); sibling of REQ-PICKER-NODE-GROUPING (the picker half of the same dedup law).
2026-09-06T13:09:01.8952267Z - Required stages: impl, unit
2026-09-06T13:09:01.8952295Z 
2026-09-06T13:09:01.8952399Z ### REQ-ENDPOINT-LIST-REST-FILTER
2026-09-06T13:09:01.8954810Z - Title: spt endpoint list hides SUSPENDED instances by default; a new --show-all flag reveals them. Status-first row ordering with fixed precedence ONLINE > CONTROLLED > UNBOUND > SUSPENDED (when shown) > corrupt last, alphabetical by id within each band. Two invariants: (1) CORRUPT rows ALWAYS render regardless of filters — corrupt is a record condition demanding operator action (purge/re-mint), not resting clutter; hiding it would re-create counter-39 bug #3 (cross-ref REQ-HAZARD-CORRUPT-PERCH-COHERENCE, CONTEXT.md instance-state _Also avoid_); (2) the per-node Total line DISCLOSES the filter — 'Total: N (+M suspended hidden)' — so nothing silently vanishes. Registry-Offline rows stay excluded by projection law (resource_projection skips unroutable; unchanged). Grill-with-docs ruling 2026-07-02 (operator + doyle).
2026-09-06T13:09:01.8954927Z - Required stages: impl, unit
2026-09-06T13:09:01.8954956Z 
2026-09-06T13:09:01.8955057Z ### REQ-ADAPTER-UPDATE-INPLACE
2026-09-06T13:09:01.8956477Z - Title: Bug #18: spt adapter update fails at re-register with os error 2 because it derives the install dir from the update repo NAME (_github/<safe>) instead of updating in place at the adapter record source_dir; when the adapter repo is intentionally renamed across releases (spt-claude-code to claude-spt, supported), the derived dir is fresh/empty and re-register reads a missing manifest. Fix: adapter update installs and re-registers in place at the registered source_dir and tolerates a changed update repo/URL across a rename. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #18.
2026-09-06T13:09:01.8956650Z - Required stages: impl, unit
2026-09-06T13:09:01.8956679Z 
2026-09-06T13:09:01.8956773Z ### REQ-DIGEST-PROFILE-ENV
2026-09-06T13:09:01.8959246Z - Title: Bug #17: spt endpoint digest returns NO_DIGEST for a ccs-profile endpoint (claude-spt:ccs) though [digest] is wired and the transcript exists — under .ccs (CLAUDE_CONFIG_DIR relocation) not .claude. The on-demand digest runs the extractor in the daemon context WITHOUT the endpoint profile transcript-location env, so the env-aware resolver cannot find the relocated transcript. Fix: propagate/persist the endpoint profile transcript-location env (e.g. the ccs CLAUDE_CONFIG_DIR) to the on-demand digest extractor so a profile-relocated transcript resolves; confirm the exact extractor verdict via spt adapter digest-proof. Ownership spt-core (digest env/profile propagation), possibly with a claude-spt extractor-resolver assist. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #17.
2026-09-06T13:09:01.8959359Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.8959383Z 
2026-09-06T13:09:01.8959484Z ### REQ-DIGEST-FETCHER-STRATEGY
2026-09-06T13:09:01.8963222Z - Title: Bug #17 (W6b, closes eel-a end-to-end): [digest] gains a `fetcher` strategy mirroring [history]'s locate/normalize split (CONTEXT §history: [digest] mirrors history's two strategies — locate ownership). ROOT: the pre-W6b [digest] had only the locate_normalize analog (spt-core resolves ONE `source` template + pre-reads the file), which CANNOT express a PARTITIONED transcript layout — CC's projects/<munge(cwd)>/<session_id>.jsonl or a date-globbed rollout tree — the exact case CONTEXT already assigns to the adapter. spt-core (correctly) provides NO {project}/slug key (harness-specific cwd munging = the charter violation FIX-A was rejected for). Fix: strategy = fetcher makes the ADAPTER's extractor locate + read + emit normalized records; spt-core runs it bounded (no locate, no pre-read, no stdin) and consumes stdout, feeding only the harness-NEUTRAL inputs it owns — {session_id}, the perch-bound {cwd} (info.json.cwd), and the captured [env] direction=read vars (W6/REQ-DIGEST-PROFILE-ENV) — so the extractor globs the unique {session_id} under {read-var-root}/projects/ with no slug. Keeps locate_normalize (default, back-compat) for a trivial single-file harness. Distinct capability from REQ-DIGEST-PROFILE-ENV (which supplies the root env). See docs/NEXT-MILESTONE-BUG-TRIAGE.md #17.
2026-09-06T13:09:01.8963399Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8963423Z 
2026-09-06T13:09:01.8963536Z ### REQ-ADAPTER-ADD-SURFACE-ERRORS
2026-09-06T13:09:01.8964987Z - Title: Bug #1: adapter add runs the install-as-first-update via conduct (cli.rs:6963) which on a non-zero exit prints only the exit code and DISCARDS the subprocess stdout/stderr, so the real error is invisible (the failure itself does propagate). Fix: include out.stderr/stdout in the ADAPTER_INSTALL_FAIL message (mirror run_update_post_step). Operator ruling: ALSO run the [update.post] composite step at install-time (today it runs only on explicit adapter update), so an install both surfaces detail and completes the delegated post-step. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #1.
2026-09-06T13:09:01.8965091Z - Required stages: impl, unit
2026-09-06T13:09:01.8965119Z 
2026-09-06T13:09:01.8965223Z ### REQ-ENDPOINT-LIST-NODE-IDENT
2026-09-06T13:09:01.8966523Z - Title: Bug #5: spt endpoint list local section header is the hardcoded literal LOCAL (this node) (render_local_section cli.rs:4359). Change to 'This node: <node-id>' using the existing node-ident idiom (os_hostname + nodeid public-key prefix, cli.rs:5531 — factor a node_ident_display helper); compute in the impure print_local_section, pass into the pure renderer. Update the two test assertions (cli.rs:10711/10716). See docs/NEXT-MILESTONE-BUG-TRIAGE.md #5.
2026-09-06T13:09:01.8966680Z - Required stages: impl, unit
2026-09-06T13:09:01.8966709Z 
2026-09-06T13:09:01.8966819Z ### REQ-HAZARD-CONTROL-STAMP-LIFETIME
2026-09-06T13:09:01.8969491Z - Title: #2: a control/viewer stamp never outlives its session — every teardown path clears what attach stamped. The broker exit-waiter (broker.rs ~:1844) sends the exit frame + sessions.remove(&id) but does NOT clear the perch's controller/viewer stamps; clear_controller()->stamp_driven_by() (clears driven_by+controlled) runs ONLY on controller-detach/evict/displace. /exit kills the CHILD not the controller conn, so the OutputLog drops with controlled:true, viewer_count, (and driven_by for a remote controller) latched in info.json forever — and hfenduleam keeps gossiping controller_node=self cross-node. Fix: on session reap, clear the perch's controller/viewer stamps (set_driven_by(None)+set_controlled(false)+set_viewer_count(0) via the known endpoint id) — broker stays the single writer. KNOWN-HAZARDS invariant. See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #2.
2026-09-06T13:09:01.8969724Z - Required stages: doc, impl, int
2026-09-06T13:09:01.8969753Z 
2026-09-06T13:09:01.8969848Z ### REQ-PICKER-CONTROLLED-LOCAL
2026-09-06T13:09:01.8972883Z - Title: #3 local half: a LOCALLY-controlled endpoint renders CONTROLLED in its own node's picker. display_status() (crates/spt/src/picker/model.rs:415) derives Controlled ONLY from driven_by.is_some(), but the legacy LOCAL attach path left driven_by=None (the remote-only reading stated here was falsified by doyle ruling v3 2026-07-19 — driven_by names the controlling node, own included; KH 7.15 governs CLEARING it, not who may latch it) — so a locally-controlled endpoint of that era has driven_by=None + controlled=true, and local_rows (data.rs:220) never threads controlled into EndpointRow, so a locally-RC'd endpoint shows plain ONLINE in its own picker (remote rows are fine — gossip stamps controller_node=self, REQ-GOSSIP-CONTROLLED-ANY; the asymmetry is the bug). Fix: EndpointRow gains controlled:bool (local: rec.controlled; remote: controller_node.is_some()); display_status -> Controlled when driven_by.is_some()||controlled; desc pane says 'controlled locally' when the driver is unnamed. See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #3.
2026-09-06T13:09:01.8973049Z - Required stages: impl, unit
2026-09-06T13:09:01.8973077Z 
2026-09-06T13:09:01.8973199Z ### REQ-PICKER-PROJECT-HISTORY-TRUTH
2026-09-06T13:09:01.8975865Z - Title: #1: picker project history is derived from sessions.log cwds (newest->oldest, deduped by project_id_for_dir) UNION context-store branches, EXCLUDING owlery-internal paths (any cwd under spt_home()/owlery) everywhere a project is displayed or inferred. Fixes three stacked defects (crates/spt/src/picker/data.rs): (1a) project_history_for (data.rs:372) reads ONLY context-store p-* branches, which are empty on this box -> history []; (1b) the fallback origin project (data.rs:207) is derived from info.json.cwd = latest-boot-cwd (rewritten every rebind), not origin; (1c) psyche-host sessions bind owlery-internal cwds that pollute history. Full DIRS stay available in the model (feature #5 needs them). PROJECT REPRESENTATION RULING (operator 2026-07-03): project IDs ONLY, EVERYWHERE incl local display; on ID collision disambiguate minimally via a PURE disambiguate_project_ids(entries)->display-names fn (append one-level-up parent folder and/or root drive letter, e.g. 'spt-core (projects)' vs 'spt-core (D:)'). See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #1.
2026-09-06T13:09:01.8975986Z - Required stages: impl, unit
2026-09-06T13:09:01.8976015Z 
2026-09-06T13:09:01.8976115Z ### REQ-STORE-CONTEXT-BRANCH-FILL
2026-09-06T13:09:01.8977588Z - Title: #1 SI-1 (RCA, operator-promoted 2026-07-03): the context store (tracked/.seed.git) holds ZERO p-* branches on a box with months of live-agent use, while kitsubito/enlyzeam stores carry them. Context commits never land -> picker history has no store source (REQ-PICKER-PROJECT-HISTORY-TRUTH's fallback ships regardless, but the store must ALSO fill). RCA the contextstore write->branch-commit path with evidence (commune-ingest/context-commit regression vs store re-init), contrast the healthy stores, land whatever fix the RCA names. See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #1 SI-1.
2026-09-06T13:09:01.8977747Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.8977776Z 
2026-09-06T13:09:01.8977941Z ### REQ-GOSSIP-ADAPTER-PROJECTS
2026-09-06T13:09:01.8980499Z - Title: #4: remote endpoint details (harness + project history) are gossiped, not faked. Today from_resource_row (crates/spt/src/picker/model.rs:340) hardcodes project_history=Vec::new() for every remote row and passes adapter_profile=row.resources (the blurb masquerading as the harness), and Instance/ResourceRow (crates/spt-net/src/net/registry.rs:457) carry no adapter field and no project list. Fix: additive gossip fields N-1-safe exactly like endpoint_type — Instance.adapter (composite <adapter>[:profile]) + Instance.recent_projects (bounded, newest-first, project IDs only) -> thread to ResourceRow -> from_resource_row stops faking. Pre-field remote rows render '-'. Project IDs only + REQ-PICKER-PROJECT-HISTORY-TRUTH's disambiguation. See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #4.
2026-09-06T13:09:01.8980614Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8980643Z 
2026-09-06T13:09:01.8980742Z ### REQ-ENDPOINT-LIST-PROJECT-COL
2026-09-06T13:09:01.8982639Z - Title: #8: spt endpoint list gains a second column <project>/ (the endpoint's LATEST project) -> 4 columns total: id / <project>/ / type / status. Local rows: head of REQ-PICKER-PROJECT-HISTORY-TRUTH (sessions.log-derived, owlery-excluded). Remote rows: head of REQ-GOSSIP-ADAPTER-PROJECTS recent_projects. Project IDs only + #4 disambiguation; '-' when unknown (pre-field remote rows). Extends the v0.21.0 node-grouped renderer (format_instance_rows — additive column, alignment char-width-safe). --json: additive project field on the row DTO (skip-if-none, N-1 safe). Depends on #1 + #4. See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #8.
2026-09-06T13:09:01.8982796Z - Required stages: impl, unit
2026-09-06T13:09:01.8982824Z 
2026-09-06T13:09:01.8982916Z ### REQ-API-ENDPOINT-INFO
2026-09-06T13:09:01.8985858Z - Title: #7: spt api endpoint-info [<id>] (JSON) lets an endpoint learn its ATTACHED (controlling) node — claude-spt surfaces local + attached node names on UserPromptSubmit so the agent knows whether getting a file to the user needs extra steps (user RC'd in from another machine). spt api * is the harness-contract agent-facing surface (JSON-first, rides perch identity/auth so the bare no-<id> form self-resolves like whoami). Payload (committed DTO, additive-forever): { id, endpoint_type, adapter, local_node:{label,key}, attached_node:{label,key}|null, controlled:bool, project:<current project id>, cwd, subnets:[...] } — attached_node from controller stamps (driven_by remote / self-node when controlled with no remote driver), null when uncontrolled. HARD dependency on #2 + #3 (stamps must be honest first). Adapter-side consumable -> perri release-ping on publish. Naming: chose 'spt api endpoint-info' over alt 'spt endpoint get-info' — api is the agent surface (doc rationale). See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #7.
2026-09-06T13:09:01.8985973Z - Required stages: impl, unit
2026-09-06T13:09:01.8986002Z 
2026-09-06T13:09:01.8986113Z ### REQ-PICKER-START-PROJECT-CHOICE
2026-09-06T13:09:01.8987919Z - Title: #5: after 'Start now' in the endpoint picker, swap the bottom Options panel to a 'Choose project' list: (1) the endpoint's most recent project dir, (2) 'Here: <dir>' — the picker's launch cwd (only if different), (3) all other project-history dirs newest->oldest. Fire the step ONLY when (A) the run cwd mismatches a singular history entry, or (B) history has >1 entry; otherwise start immediately (today's behavior). Depends on REQ-PICKER-PROJECT-HISTORY-TRUTH (needs full DIRS from sessions.log, owlery-internal exclusion applies). Start-now is local-only (no gossip). See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #5.
2026-09-06T13:09:01.8988082Z - Required stages: impl, unit
2026-09-06T13:09:01.8988110Z 
2026-09-06T13:09:01.8988215Z ### REQ-PICKER-RESUME-CONTEXT-PANEL
2026-09-06T13:09:01.8989494Z - Title: #6: the 'Resume from history' view keeps the endpoint's 'Confirm selection' top panel and swaps ONLY the bottom panel to 'Resume from a prior session' — the user stays contextually informed about what they're picking (today the resume view replaces the whole screen). crates/spt/src/picker/view.rs (resume screen) + model screen state. See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #6.
2026-09-06T13:09:01.8989696Z - Required stages: impl, unit
2026-09-06T13:09:01.8989724Z 
2026-09-06T13:09:01.8989823Z ### REQ-MSG-SELF-DETECT-ANCESTRY
2026-09-06T13:09:01.8993084Z - Title: #9 (F026, operator field bug): a perch-owned `spt send` from an endpoint's OWN session must self-identify, not mis-stamp `cli@<node>` (whose replies bounce NO_PERCH). ROOT: roster::detect_self_id (roster.rs) was ENV-ONLY — OWL_SESSION_ID matched to info.session_id, else SPT_AGENT_ID — but an agent-session Bash child often carries NEITHER (the env export is spawn-path-dependent), so a perch-owned sender was classified bare-CLI and REQ-MSG-CLI-ORIGIN stamped it cli@<node> (the stamp works as designed on a wrong premise; that REQ's evidence stays intact). FIX: detect_self_id gains leg (c) PID-ANCESTRY fallback AFTER the env legs — walk THIS process's ancestry, match a live non-corrupt roster perch's recorded harness pid (info.json.pid Numeric, alive-gated via is_process_alive, corrupt/BUSY skipped), first match = self. from-LABEL / routing default ONLY, NOT authentication — authenticate() is untouched (pid-ancestry-for-AUTH stays parked per F-024 with its Windows pid-spoof caveats; a display/routing stamp has no such bar). Best-effort: a broken ancestry walk degrades to None → cli-stamp, never errors the send.
2026-09-06T13:09:01.8993290Z - Required stages: impl, unit, int
2026-09-06T13:09:01.8993318Z 
2026-09-06T13:09:01.8993419Z ### REQ-CLI-BROKEN-PIPE-TOLERANT
2026-09-06T13:09:01.8994851Z - Title: #10 (F026 micro): `spt <anything> | <pager/Select-First-N>` must not PANIC when stdout closes early. A closed downstream reader mid-print panics with 'failed printing to stdout: The pipe is being closed. (os error 232)' (live repro: `spt daemon status | Select -First N`). Fix: tolerate BrokenPipe process-wide — a write to a closed stdout exits 0 (SIGPIPE-equivalent: a consumer that stops reading is a normal end, not a crash), without leaking a Rust panic + backtrace to the user.
2026-09-06T13:09:01.8994959Z - Required stages: impl, unit
2026-09-06T13:09:01.8994987Z 
2026-09-06T13:09:01.8995085Z ### REQ-XTASK-SPT-BIN-TARGET-DIR
2026-09-06T13:09:01.8997322Z - Title: #13 (F026 micro, tooling): xtask `spt_bin()` (crates/xtask/src/main.rs) BUILDS `spt` via cargo (which honors CARGO_TARGET_DIR) but returns a HARDCODED `<root>/target/debug/spt` path — so under a redirected target dir (CI / isolated-gate rigs that set CARGO_TARGET_DIR to a throwaway) the binary lands in `$CARGO_TARGET_DIR/debug` while xtask looks in `<root>/target/debug` -> NotFound -> `xtask check` (docs-drift gate) spuriously fails. Workaround was running `xtask check` with CARGO_TARGET_DIR unset. FIX: a pure `target_debug_dir(root, CARGO_TARGET_DIR)` seam mirroring cargo's resolution — absolute override as-is, relative resolved against `root` (the dir cargo is invoked in), default `<root>/target` — join `debug`; `spt_bin` returns from it. See docs/NEXT-MILESTONE-PICKER-TRIAGE.md.
2026-09-06T13:09:01.8997469Z - Required stages: impl, unit
2026-09-06T13:09:01.8997497Z 
2026-09-06T13:09:01.8997613Z ### REQ-HAZARD-CONTROL-STAMP-CONVERGENCE
2026-09-06T13:09:01.9002030Z - Title: Control/viewer stamps must CONVERGE to broker session-table truth for every session-backed endpoint, not merely edge-trigger — the UPWARD companion to the DOWNWARD edge-clear REQ-HAZARD-CONTROL-STAMP-LIFETIME (7.27); same 'stamps == broker truth' family. ROOT (F-026 stamp-gap, hall-b/ball-b): a picker-created endpoint's broker spawn become_controller->stamp_driven_by->set_controlled(true) fires BEFORE the adapter binds its perch (a fresh endpoint has no perch until claude boots + binds), so mutate_info returns NotFound and the edge stamp is SWALLOWED (let _); the adapter's bind then writes InfoJson::new with controlled:false DEFAULT and no later edge re-stamps -> the endpoint reads uncontrolled FOREVER while driven (the #3 display fix is correct but datum-starved on this creation path). FIX: the broker (SINGLE WRITER) re-asserts each live session's control/viewer stamps to session-table truth, DIVERGENCE-GATED (read info; compare driven_by/controlled/viewer_count; write ONLY on diff — no per-poll fsync storm), on the KIND_SESSIONS handler (piggyback: the daemon reconcile + picker poll it, so a fresh perch converges within one reconcile-poll window after bind = the BOUNDED window, no new timer). Event-on-input rejected (an idle controlled session like hall-b never converges). Writes run OFF the log lock (snapshot truth under the lock, converge off it) per the lock-across-effect discipline (KH 7.12/5.16).
2026-09-06T13:09:01.9002304Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9002333Z 
2026-09-06T13:09:01.9002428Z ### REQ-PICKER-PROJECT-DISPLAY-NAME
2026-09-06T13:09:01.9004880Z - Title: A1 (F028, operator #1/#4/#6-display): `github-com-*` 'ghost' project entries are NOT phantoms — project_id_for_dir (spt-store/src/project.rs:64) derives ids from the git remote slug BY DESIGN (REQ-STORE-1 cross-machine sync): `github.com/SaberMage/spt-core` -> `github-com-sabermage-spt-core`. The ref is truthful; the BUG is presentation — the raw slug renders as the DISPLAY NAME everywhere (confirm-panel history view.rs:415-419, choose-project labels model.rs:314-337, resume-row titles, endpoint-list project column via latest_project_ref data.rs:417), which no operator recognizes as 'spt-core'. FIX: keep the slug as the KEY, render a friendly display name — the repo tail (spt-core) reusing the disambiguate_project_ids (model.rs:346) suffix mechanism for collisions. One shared display-name seam across all four surfaces. See triage A1.
2026-09-06T13:09:01.9005034Z - Required stages: impl, unit
2026-09-06T13:09:01.9005063Z 
2026-09-06T13:09:01.9005171Z ### REQ-PICKER-CONTROL-LINE-STATUS-GATE
2026-09-06T13:09:01.9006788Z - Title: A2 (F028, operator #2): the picker confirm-panel 'controlled locally' line renders for OFFLINE endpoints. view.rs:425-436 builds control_line from ep.controlled with NO status gate; an offline endpoint with a stale controlled stamp shows 'controlled locally' (operator screenshot: hall-a offline + controlled locally). RENDER HALF (this REQ): control_line MUST be empty when status != Online. The upstream STICKY-stamp half (stamp survives client SIGKILL >=5min) is B3/REQ-PRESENCE-CONTROL-REAP-ON-EXIT. FIX: gate the render. See triage A2(a).
2026-09-06T13:09:01.9006951Z - Required stages: impl, unit
2026-09-06T13:09:01.9006988Z 
2026-09-06T13:09:01.9007127Z ### REQ-PICKER-OFFLINE-NO-VIEW
2026-09-06T13:09:01.9008459Z - Title: A3 (F028, operator #3): 'View now (read-only)' is offered for OFFLINE endpoints. model.rs:1030 offline branch of confirm_options is vec![Start, View] — View is meaningless with no live PTY. FIX: offline set = [Start] (+ the existing Resume/ChangeAdapter/Instantiate/Fork/Shortcut tail). Update the view.rs options tests. See triage A3.
2026-09-06T13:09:01.9008612Z - Required stages: impl, unit
2026-09-06T13:09:01.9008649Z 
2026-09-06T13:09:01.9008807Z ### REQ-PICKER-CHOOSE-DEDUP-ALL
2026-09-06T13:09:01.9010701Z - Title: A4 (F028, operator #5): choose-project duplicate rows. model.rs:314-337 build_project_choices dedups the `Here: <run_cwd>` row only against the HEAD ref's dir (line 324) — an OLDER history ref with the SAME dir still renders, giving `Here: C:\...\projects` + `projects` as two rows for one project (operator screenshot). FIX: dedupe `Here` against ALL history dirs, and skip rest-rows whose dir == run_cwd when Here is present. Extend the model.rs:1847 choose-project test. See triage A4.
2026-09-06T13:09:01.9010915Z - Required stages: impl, unit
2026-09-06T13:09:01.9010944Z 
2026-09-06T13:09:01.9011045Z ### REQ-RESUME-ROW-PER-PROJECT
2026-09-06T13:09:01.9012193Z - Title: A5 (F028, operator #6): resume-from-history labels EVERY session with the endpoint's newest project. data.rs:480-496 resume_rows_for clones project_history.first() onto every ResumeRow (line 481/488), so all sessions read as the head project (the ghost). The per-row e.cwd is already carried for launch-into-dir. FIX: derive per-row project_id_for_dir(e.cwd) (owlery-excluded -> fall back to trigger token), rendered through A1's display-name path. See triage A5.
2026-09-06T13:09:01.9012389Z - Required stages: impl, unit
2026-09-06T13:09:01.9012427Z 
2026-09-06T13:09:01.9012522Z ### REQ-RUN-NO-DUP-SESSION
2026-09-06T13:09:01.9015129Z - Title: B1 (F028, hall-b diagnosis, verified 0.22.0): `endpoint run --id X --create` on an endpoint with a LIVE session mints a silent DUPLICATE session — and attach output can CROSS sessions (second create for diag-hallc minted a new session while the old ran; the new run's attach viewport rendered the OLD session's screen — claude resume-picker UI of pid 84512 while new claude 356020 had no -r). ROOT CLASS of the 0.21.0 attach-stall (zero events in FIRST_EVENT_GRACE rc.rs:1402 = attach bound to dead/wrong same-id slot); also the triplicate `launch --id ball-b` on ENLYZEAM. FIX: (i) run-on-live-session must REFUSE or REATTACH, never silently duplicate; (ii) RCA the attach/output routing that let frames cross same-id sessions (broker session-slot keying, dispatch_adapter vs serve_attach resolution). Int: two sessions one endpoint id -> each attach sees only its own frames. See triage B1.
2026-09-06T13:09:01.9015243Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9015271Z 
2026-09-06T13:09:01.9015374Z ### REQ-RESUME-HARNESS-SESSION-ID
2026-09-06T13:09:01.9017896Z - Title: B2 (F028, hall-b diagnosis, verified 0.22.0): respawn/`--resume` feeds the SPT session id to `claude -r`. After the 02:05 daemon bounce respawn built `claude.exe -r 70b5bfa40901b7d4` — an spt session id in claude's OWN session-id namespace -> claude hangs forever at a 'No sessions match' resume-picker while the endpoint reads online. Hits after EVERY daemon bounce + every picker Resume. The HARNESS session id (claude UUID, stamped in sessions.log/info.json by the hooks) is what {session_id} must mean in the adapter's [session.resume] command; the spt sid must not leak. FIX: substitute the HARNESS session id in the resume template (spt-core substitution-key semantics + LIKELY claude-spt manifest coordination — FLAG perri BEFORE touching the manifest, adapter-boundary rule). Int: resume template receives the ledger UUID, not the spt sid. See triage B2.
2026-09-06T13:09:01.9018005Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.9018043Z 
2026-09-06T13:09:01.9018153Z ### REQ-PRESENCE-CONTROL-REAP-ON-EXIT
2026-09-06T13:09:01.9020942Z - Title: B3 (F028, hall-b diagnosis + deferred #11 seed; BROADENED perri F-b CONFIRMED): dead-pid ONLINE decay window + sticky CONTROLLED stamp. Repro: /exit -> all endpoint processes dead -> `endpoint list` stays ■ ONLINE for a decay window before OFFLINE. Sticky CONTROLLED: perri confirmed controlled=true + attached_node SET while alive=false/OFFLINE, persisting >20min AND ACROSS A DAEMON RESTART (hall-b) — worse than the SIGKILL>=5min original (CAVEAT still: may reflect claude's --remote-control channel not the PTY attach — DISAMBIGUATE first). This is the deferred #11; RCA belongs to this wave. FIX: reap must clear presence AND control stamps promptly across FOUR paths — (i) clean exit, (ii) serve conn-drop, (iii) session-died-without-exit (crash/bounce), (iv) a BOOT-TIME sweep so a restarted daemon does NOT resurrect control stamps for endpoints it can see are dead. Int tests per edge. Closes A2(b). See triage B3 (broadened).
2026-09-06T13:09:01.9021118Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9021146Z 
2026-09-06T13:09:01.9021251Z ### REQ-RESUME-REAP-PRIOR-HARNESS
2026-09-06T13:09:01.9022244Z - Title: B4 (F028, hall-b diagnosis, verified 0.22.0): `--resume` respawns a SECOND harness onto the SAME session without reaping the first. Observed live: resume of b4421cf9 spawned pid 34432 while gen1 (250376) kept running — two claude.exe stacks, one session id. FIX: resume must reap/refuse when the session already has a live harness. See triage B4.
2026-09-06T13:09:01.9022348Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9022467Z 
2026-09-06T13:09:01.9022564Z ### REQ-CRC-SWAP-OLD-DISPLACE
2026-09-06T13:09:01.9025232Z - Title: C1 (F028, infra; memory crc-swap-old-litter-brick, RCA'd ENLYZEAM + confirmed systemic): crc_swap `.old`-litter bricks every subsequent adapter update. apply_crc_swap Phase-3 `let _ = remove_file(.old)` (spt-daemon/src/crc_swap.rs:129-133) silently fails whenever ANY pre-update process still image-maps the old binary (NORMAL on a live box — endpoint launch children survive updates). The NEXT update's first commit-op rename(exe->exe.old) = MoveFileExW(REPLACE_EXISTING) must delete the mapped .old -> win32 err 5 -> whole apply fails + rolls back FOREVER, context-free. FIX: (i) DISPLACE not replace — when <target>.old exists, rename it aside to a unique suffix (rename succeeds on mapped files; spt's own updater already does spt.exe.old-<counter>); GC stale .old.* opportunistically. (ii) Wrap swap io errors with op + path (`rename claude-spt.exe -> claude-spt.exe.old: …`). Unit seam exists (crc_swap tests). See triage C1.
2026-09-06T13:09:01.9025332Z - Required stages: impl, unit
2026-09-06T13:09:01.9025360Z 
2026-09-06T13:09:01.9025476Z ### REQ-SESSIONS-LOG-ENDPOINT-ATTRIBUTION
2026-09-06T13:09:01.9028099Z - Title: C2 (F028, infra; ROOT-CAUSED + severity-upgraded doyle RCA 2026-07-03): cross-endpoint perch contamination — a foreign psyche's SessionStart hook REBINDS a victim perch's IDENTITY, not merely its ledger. Evidence: hall-a's info.json.session_id IS f015b-probe-psyche's session (359d7bd7) + hall-a's ledger holds the foreign psyche session; same class as hall-b's dead-pid stamp (141556). This REQ = the OBSERVABLE (foreign session_id in a perch's ledger/info.json + resume offering foreign sessions) and its belt-braces: (iii) filter owlery-cwd rows OUT of resume_rows; (iv) one-time repair for already-contaminated perches (hall-a on HFENDULEAM) or self-heal on next legitimate session-start. The ROOT (identity pinned at spawn + honest bind + nested self-resolve) is REQ-BIND-HONEST-SELF-STAMP — C2 is UPSTREAM of B3 (presence/CONTROLLED read the very stamps this corrupts). See triage C2.
2026-09-06T13:09:01.9028262Z - Required stages: impl, unit
2026-09-06T13:09:01.9028286Z 
2026-09-06T13:09:01.9028391Z ### REQ-BIND-HONEST-SELF-STAMP
2026-09-06T13:09:01.9032565Z - Title: C2-ROOT (F028, doyle RCA 2026-07-03): the identity-attribution ROOT behind REQ-SESSIONS-LOG-ENDPOINT-ATTRIBUTION — three composing defects let a psyche-hosted SessionStart hook stamp a FOREIGN perch: (1) roster::detect_self_id leg (a) scans the owlery ONE level (roster.rs:107 read_dir(owlery)) so a NESTED psyche perch owlery/<parent>/nested/<id>-psyche can never self-resolve; (2) [session.psyche_init] (claude-spt manifest ~L347) spawns the psyche with NO env_remove + no pinned child identity, so whatever OWL_SESSION_ID/SPT_AGENT_ID reaches the child resolves to a foreign top-level perch; (3) the hook then writes info.json (session_id/pid rebind) + sessions.log on the mis-resolved victim. FIX (spt-core half): (i) identity PINNED at spawn — ManifestRuntime role spawns inject the child's OWN SPT_AGENT_ID=<child perch id> + OWL_SESSION_ID=<child session>, AND detect_self_id enumerates NESTED perches (fix the one-level owlery scan); (ii) BIND HONESTY — a session-start stamp may only write a perch whose resolved id AFFIRMATIVELY matches; never a fallback pick; refuse + loud-skip when unresolved (kin REQ-MSG-CLI-ORIGIN honest-default + #9 ancestry). ADAPTER half = PERRI touchpoint (psyche_init env scrubbing / relies on runtime pinning; hook loses silent fallback-perch behavior) — FLAG doyle BEFORE any manifest move (adapter glue-model rule). See triage C2 fix (i)+(ii).
2026-09-06T13:09:01.9032751Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9032780Z 
2026-09-06T13:09:01.9032876Z ### REQ-PEER-PUMP-CHURN-STALL
2026-09-06T13:09:01.9035139Z - Title: B5 (F028, perri F-a; DEFECT daemon, OBSERVED-ONCE, HIGH): the peer pump STALLS under rapid rc attach/EOF-detach/--take churn. Fresh 0.22.0 daemon ~10min after restart, during rapid rc cycling: `peer pump: STALLED (last tick 122s)`; while stalled `spt rc --view` -> `RC_FAIL: attach request: brain IPC read deadline elapsed` (repeatable) and controlled-clear stopped propagating. Daemon restart recovered + endpoints auto-revived. Prior class: REQ-HAZARD-PUMP-IPC-DEADLINE (reader-thread+channel carrier), REQ-broker-QUIC-deadline (bounded_block_on) — something in the rc-churn path can still wedge the pump tick. perri holds exact timestamps + a repro candidate (rapid attach/detach/take against one endpoint) — REQUEST before RCA. See triage B5.
2026-09-06T13:09:01.9035496Z - Required stages: 
2026-09-06T13:09:01.9035525Z 
2026-09-06T13:09:01.9035630Z ### REQ-TRANSLATE-BINARY-LIVENESS-DECAY
2026-09-06T13:09:01.9038382Z - Title: SUPERSEDED by REQ-TRANSLATE-COMMIT-MISS-TOLERANCE (F029 C-1). B6 (F028, perri F-e) was ROOT-PINNED as the commit-deadline-miss fault: at a checkpoint clear boundary the clear-only inject's {commit} was never observed within INJECT_COMMIT_DEADLINE, so the inject worker FAULTED + TERMINATED a HEALTHY translate binary and (by ADR-0022) never respawned → every subsequent force-native reported delivered=false ('no live translation binary'). NOT a dormancy/liveness-registration decay (that hypothesis is dead) — deterministic at every checkpoint-armed boundary. The fix (miss != fault + N=3 strike budget + bounded respawn + perch-visible fault stamp) lives under REQ-TRANSLATE-COMMIT-MISS-TOLERANCE + REQ-HAZARD-TRANSLATE-FAULT-PERMANENT-DEATH. See triage addendum C-1.
2026-09-06T13:09:01.9038575Z - Required stages: 
2026-09-06T13:09:01.9038607Z 
2026-09-06T13:09:01.9038721Z ### REQ-PSYCHE-CRASHLOOP-BACKOFF-SHUTDOWN
2026-09-06T13:09:01.9040587Z - Title: C3 (F028, perri F-h): psyche wrapper crash-loop has no backoff, and `endpoint shutdown` misses a wedged wrapper. A probe psyche crash-looped ~3 boots/sec for ~30min (CC died instantly on the untrusted owlery cwd; ledger hit ordinal 5358) — SILENTLY; and `spt endpoint shutdown` did NOT tear the looping wrapper down (docs say shutdown tears the Psyche with the perch; manual kill was required). FIX: (i) bounded backoff + loud give-up on a psyche boot loop (the psyche_host_error surface already exists), (ii) shutdown must cover a wedged/looping wrapper. See triage C3.
2026-09-06T13:09:01.9040687Z - Required stages: 
2026-09-06T13:09:01.9040715Z 
2026-09-06T13:09:01.9040817Z ### REQ-DOC-ENDPOINT-DROP-RESOLUTION
2026-09-06T13:09:01.9041990Z - Title: D1 (F028, perri F-c; docs/truth): SI-1's resolution rule — a RELATIVE watched drop dir resolves against the ENDPOINT's cwd, never the daemon's (KH 7.28, shipped v0.22.0) — is documented NOWHERE public. Add it to harness-contract/manifest.md + the manifest schema field descriptions so an adapter author knows a relative commune_dir/signoff_dir is endpoint-resolved. docs-drift gate applies. See triage D1.
2026-09-06T13:09:01.9042091Z - Required stages: 
2026-09-06T13:09:01.9042119Z 
2026-09-06T13:09:01.9042220Z ### REQ-DAEMON-STATUS-JSON-TRUTH
2026-09-06T13:09:01.9043741Z - Title: D2 (F028, perri F-d): `daemon status --json` truth drift. managed_by/managed_active read null in JSON while the HUMAN view says 'managed-by: manual — at-logon task registered' (the two surfaces disagree); and pump staleness (the STALLED diagnosis, B5) is NOT computable from JSON — only a raw pump_heartbeat_ms is emitted, no derived staleness/stalled field. FIX: JSON managed_by/active match the human render, and add a derived pump-staleness/stalled field so B5's condition is machine-observable. See triage D2.
2026-09-06T13:09:01.9043994Z - Required stages: 
2026-09-06T13:09:01.9044023Z 
2026-09-06T13:09:01.9044126Z ### REQ-ENDPOINT-LIST-RENDER-POLISH
2026-09-06T13:09:01.9046361Z - Title: A6 (F028, operator, 4 asks): `spt endpoint list` render polish. (a) the 'Shared subnets' line is NOT dim — LIGHT_GRAY = "37" (cli.rs:3019) is standard-palette WHITE, indistinguishable from row text; use SGR 90 (bright-black/gray) for the dim intent. (b) the `Total:` line takes the same dim color. (c) move the status glyph ADJACENT to the endpoint name (operator: 'right behind the endpoint name'), mirroring the picker's glyph-beside-name presentation (today the glyph sits at the end next to the status word). (d) color the status WORD like the picker TUI (green ONLINE / gray OFFLINE / blue when driven, matching picker glyph semantics). All in render_node_grouped/render_instance_row (cli.rs ~3000s); pure render with an injected color decision — unit-testable off a tty. See triage A6.
2026-09-06T13:09:01.9046578Z - Required stages: impl, unit
2026-09-06T13:09:01.9046607Z 
2026-09-06T13:09:01.9046719Z ### REQ-ENDPOINT-LIST-JOINED-SUBNETS-LABEL
2026-09-06T13:09:01.9051000Z - Title: THE THIS-NODE SUBNET LINE IN `spt endpoint list` SAYS JOINED, NOT SHARED (releases#72, BUGFIX). The two subnet lines this surface renders are DIFFERENT FACTS wearing one label: the This-node line lists the subnets THIS node is a member of (cmd_endpoint_list names, narrowed by --subnet), while a remote node row lists the subnets that node gossips a visible row through UNIONED ACROSS OUR MEMBERSHIPS -- the subnets we have in common with that machine. Calling the first one Shared invites the reader to look for a second party that is not there. THE REMOTE LINE KEEPS ITS NAME and that is part of the requirement rather than an omission: Shared is precise for a row whose content IS an intersection, and renaming both would trade one imprecision for another. The render docs already carry the corrected vocabulary (render_node_grouped is documented as carrying our joined subnets for this node and a Shared subnets line for the remotes), so this change makes the rendered text agree with the language the code already uses for it. THE UNIT EVIDENCE MUST DISCRIMINATE THE TWO LINES, which the pre-fix assertion could not: its fixture gave the This-node set and the remote set the SAME members, so a substring assertion matched either line and would have passed unchanged while the subject of the assertion silently changed. The repinned row asserts each label once, against fixtures whose two subnet sets DIFFER, so a build that renames the wrong line fails it. Gate: impl -- the This-node line renders Joined subnets while the remote-group line is untouched; unit -- both labels asserted by name with distinct fixtures, so neither line can be renamed, dropped or duplicated unobserved.
2026-09-06T13:09:01.9051171Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.9051200Z 
2026-09-06T13:09:01.9051314Z ### REQ-ENDPOINT-LIST-OWN-NESTED-SECTION
2026-09-06T13:09:01.9057213Z - Title: `spt endpoint list` SHOWS THE CALLING ENDPOINT ITS OWN NESTED PERCHES, in a section of their own (releases#98, ADDITION; ruled 2026-08-02). A live agent cannot see its own psyche or its own workers on the surface that exists to answer what is running here: nested perches live at owlery/<parent>/nested/<child>/ and the roster this view is built from enumerates top-level perches only, so the children are invisible by construction rather than by choice. THE NESTING RELATION IS THE STRUCTURAL ONE SPT-CORE ALREADY RECORDS -- the child dir sitting inside the parent perch dir -- AND IT IS THE ONLY SOURCE. NAME-PREFIX MATCHING IS NEVER CONSULTED, and that negative is required text rather than implied by the positive: a perch named <parent>-foo that is not nested does NOT appear, and a genuinely nested perch appears WHATEVER it is called. A build that filtered the flat roster by id prefix would satisfy a naive shows-my-psyche assertion while being wrong in both directions. THE SECTION IS THE CALLERS OWN AND NO ONE ELSES: every other agent entry renders exactly as it does today, so this adds a self-view rather than a new cross-agent disclosure. CALLER IDENTITY RIDES THE EXISTING ENV AND PROCESS-IDENTITY SEAM (roster detect_self_id: session-id match across the nested-inclusive perch enumeration, then the pinned agent and endpoint env vars accepted only when they map to a bound perch, then process ancestry) -- NO NEW IDENTITY CHANNEL IS MINTED, and nothing here confers authority: the seam is label-only by construction (KNOWN-HAZARDS 7.3/7.5) and this consumer only decides whose children to render. A CALLER THE SEAM CANNOT RESOLVE SIMPLY GETS NO SECTION -- silence, never a guessed parent whose children would then be shown to the wrong reader. Path composition for the nested namespace belongs in the single perch-layout source, not in the CLI, per the single-path-source invariant. Gate: impl -- a nested-perch enumerator keyed on the parent in the perch-layout module, the caller resolution at the gather site, and the section rendered inside the This-node group by the existing pure renderer; unit -- a nested child appears under its parent for the calling endpoint, a same-prefix NON-nested perch does NOT appear, a nested child with an unrelated name DOES, another agent nested children are absent from the callers section, and an unresolvable caller renders no section at all.
2026-09-06T13:09:01.9057464Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.9057507Z 
2026-09-06T13:09:01.9057639Z ### REQ-CLI-WIN-VT-ENABLE
2026-09-06T13:09:01.9059851Z - Title: A7 (F028, operator, Win10 conhost): ANSI emitted without VT enable → garbled console. Evidence (raw PowerShell 7, Win10 conhost): literal `←[36m` in `endpoint list` + `--help`. ROOT: ENABLE_VIRTUAL_TERMINAL_PROCESSING is enabled ONLY on the rc attach path (rc.rs:746, REQ-RC-WIN-VT-OUTPUT) — plain CLI stdout never enables it, and the color decision doesn't fall back when the console can't render VT. FIX: lift the rc.rs VT-enable into a SHARED startup helper for every colored-output path; if SetConsoleMode fails (or stdout isn't a console), STRIP colors (the ansi_wrap/helpfmt color=false path already exists — plumb the decision, not new rendering). Windows Terminal masks this (VT always on) — TEST on raw conhost. See triage A7.
2026-09-06T13:09:01.9060019Z - Required stages: impl, unit
2026-09-06T13:09:01.9060043Z 
2026-09-06T13:09:01.9060151Z ### REQ-GOSSIP-CONTROLLED-CROSS-NODE
2026-09-06T13:09:01.9062417Z - Title: B7 (F028, operator, cross-node): a remote endpoint's CONTROLLED state is not rendered. Evidence: ball-b ONLINE + CONTROLLED on ENLYZEAM (local view), but HFENDULEAM renders remote ball-b as plain ONLINE (both 0.22.0). The F-026 #4 gossiped any-controller datum (REQ-GOSSIP-ADAPTER-PROJECTS controlled bool) either isn't SENT for the locally-controlled case, isn't APPLIED on the receiving row, or DECAYS. Local leg confirmed fine (sibling hall-b renders blue-glyph correctly); the gap is the REMOTE leg. perri's validation had this ENV-BLOCKED — two live nodes now available to RCA. FIX: RCA sender-side (is controlled gossiped when locally-controlled?) / receiver-render (does from_resource_row surface it?) / decay, then lock with a cross-node int. See triage B7.
2026-09-06T13:09:01.9062524Z - Required stages: 
2026-09-06T13:09:01.9062552Z 
2026-09-06T13:09:01.9062657Z ### REQ-HAZARD-INJECT-WORKER-POISON
2026-09-06T13:09:01.9065463Z - Title: The per-session inject-worker floor Mutex is SHARED by the inject worker (open/flush) and the controller-input path (dispatch_input Layer C buffer_if_held); a panic under the lock on EITHER poisons it, and a bare .lock().unwrap() at the next site then panics too — a panic in the inject WORKER kills its thread WITHOUT reaping the translation child, orphaning a live binary while event_tx.send fails, so force-native reports 'worker-gone' delivered=false FOREVER (the F-e generic-miss shape). HARDENING, NOT the F-e incident root (perri's matrix exonerated poison under thrash/dormancy/churn): (i) poison-tolerant floor lock (unwrap_or_else into_inner) at all 3 sites so one panic can't cascade the session's delivery dead; (ii) a panic-resilient inject worker (catch_unwind -> fault+terminate the child on a worker panic) so a dead worker never orphans a live binary + strands delivery. Poison-tolerance class of REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE / bug #16.
2026-09-06T13:09:01.9065627Z - Required stages: impl, unit
2026-09-06T13:09:01.9065656Z 
2026-09-06T13:09:01.9065761Z ### REQ-HAZARD-DETACHED-DAEMON-STDIO
2026-09-06T13:09:01.9071014Z - Title: A daemon DETACHED-IN-FACT (no interactive console, or an inherited stderr PIPE nobody drains) that never nulled its std handles will BLOCK on stdio writes when the pipe fills, and/or pop a visible conhost window (REQ-HAZARD-WMI-DAEMON-WINDOW is a covered surface of this hazard). detach_console nulls the 3 handles only under the --detached flag; a rung that omits it (the bare line-82 elevated->deelevated respawn; a STALE installer at-logon task registered as bare `daemon run`, confirmed live field-drift on ENLYZEAM) is exposed. FIX: (load-bearing) inside `daemon run`, null the 3 std handles when stderr GetFileType==FILE_TYPE_PIPE — a pipe is the ONLY std sink that BLOCKS the daemon when it fills; catches every rung whose inherited stderr is an undrained pipe, independent of whether each caller passed --detached, while a FILE (2>run.log AND every int-test Stdio::from(file) brain-log capture), a CONSOLE (scrolls), and a NULL/absent handle (DETACHED_PROCESS rungs, already discard) all SURVIVE. DELIBERATELY NOT gated on GetConsoleWindow==NULL: a CREATE_NO_WINDOW daemon has no console window yet a drained FILE stderr — nulling it would blank the capture for ZERO safety gain (a file never blocks) and mass-red the int-test brain-log assertions. (belt) pass --detached on the bare line-82 respawn; (drift nag) parse the schtasks /Query action argv and LOUDLY nag when the at-logon task is the stale bare `daemon run` form (manual/installer re-registers; the daemon must NOT self-elevate to rewrite it). Defense-in-depth — no current spt Windows spawn path was proven to yield a BLOCKING inherited pipe (all rungs null-discard or scrolling-conhost), so this is hardening, not a confirmed incident root.
2026-09-06T13:09:01.9071276Z - Required stages: impl, unit
2026-09-06T13:09:01.9071305Z 
2026-09-06T13:09:01.9071404Z ### REQ-PICKER-CURRENT-DIR-LABEL
2026-09-06T13:09:01.9073742Z - Title: A-2/A-3 (F029, operator, semantic pair): the Choose-project rows must self-identify the CURRENT DIR. build_project_choices (picker/model.rs:322-352). A-2: when the run cwd IS already a history dir the `Here:` row is (correctly) suppressed by the dedup (REQ-PICKER-CHOOSE-DEDUP-ALL), but the matching history row rendered bare `r.display` with no cwd affordance — mark it `<display> (CURRENT DIR)`. A-3: the not-in-history current-dir row changes from `Here: <run_cwd>` to `CURRENT DIR --> <project>`, deriving the display the SAME way the history refs do (folder tail; honest fallback to the raw path when underivable). `cwd` payload unchanged. Grep-tests rule: 3 `starts_with("Here: ")` asserts (model.rs) + a `Here: /here` render assert (view.rs) are behavior assertions on the OLD label. See triage A-2/A-3.
2026-09-06T13:09:01.9073858Z - Required stages: impl, unit
2026-09-06T13:09:01.9073886Z 
2026-09-06T13:09:01.9073985Z ### REQ-PICKER-FORK-LABEL-CWD
2026-09-06T13:09:01.9074949Z - Title: B-3 (F029, operator): the confirm-panel `Fork endpoint` option label is static and says nothing about WHERE the fork lands. A fork runs in the picker's launch cwd (run_cwd); the label must state that dir honestly: `Fork endpoint here --> <current dir>`. Anchor picker/view.rs confirm_option_label (was `fn(opt)->&'static str`). Make the label model-aware for the dir-relative options. See triage B-3.
2026-09-06T13:09:01.9075055Z - Required stages: impl, unit
2026-09-06T13:09:01.9075078Z 
2026-09-06T13:09:01.9075187Z ### REQ-PICKER-SHORTCUT-LABEL-FILENAME
2026-09-06T13:09:01.9076798Z - Title: B-4 (F029, operator): the confirm-panel shortcut option label is a static `New/Update spt-<id> shortcut (s)` placeholder — it should name the REAL file it writes: `Set shortcut here --> <current dir>/<shortcut-name>` where <shortcut-name> is the EXACT on-disk filename (incl. extension). The name must be produced by the SAME function that names the file in shortcut creation (picker/shortcut.rs shortcut_filename over the manifest-resolved basename) so label and writer can NEVER drift. Anchor picker/view.rs confirm_option_label. See triage B-4.
2026-09-06T13:09:01.9076951Z - Required stages: impl, unit
2026-09-06T13:09:01.9076980Z 
2026-09-06T13:09:01.9077183Z ### REQ-HAZARD-ADAPTER-PROFILE-STAMP-CLOBBER
2026-09-06T13:09:01.9079890Z - Title: A-4 (F029, operator regression): the picker/confirm views drop an endpoint's adapter `:profile` (showed `claude-spt` where `claude-spt:ccs` was created). ROOT: stamp_creation_fields (spt-store/home.rs) gave the incoming BIND-TIME adapter value UNCONDITIONAL precedence (`rec.adapter = adapter.map(...).or_else(prior)`), but a hook bind resolves the adapter ADAPTER-AGNOSTICALLY (ADR-0021: a binary basename → the BARE parent, profile unknowable), so the first hook bind rewrote the richer `claude-spt:ccs` → `claude-spt`. (F-028's establish_perch self-heal widened how often this re-stamps; the precedence is the root.) FIX: profile-preserving precedence — when the incoming adapter is exactly the PARENT of the prior's `parent:profile` composite, KEEP the prior; replace only on a genuinely different adapter (or a different explicit profile). Paid-for field bug → hazard. See triage A-4.
2026-09-06T13:09:01.9080050Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9080084Z 
2026-09-06T13:09:01.9080185Z ### REQ-PICKER-WINDOW-TITLE
2026-09-06T13:09:01.9081654Z - Title: B-5 (F029, operator): `spt endpoint run`'s interactive picker window/tab is untitled — hard to find among many terminals. Set the window/tab title to `SPT Endpoint Picker`. Anchor picker/mod.rs:88 setup_terminal (crossterm SetTitle in the execute! chain). Set-only is acceptable (crossterm can't cheaply read the prior title to restore). Applies ONLY to the interactive picker path — a non-interactive/headless bringup (REQ-HOST-RUN-1) must NOT retitle the operator's terminal. See triage B-5.
2026-09-06T13:09:01.9081760Z - Required stages: impl
2026-09-06T13:09:01.9081788Z 
2026-09-06T13:09:01.9081903Z ### REQ-PICKER-KEY-GATE-LAUNCH-CAPABLE
2026-09-06T13:09:01.9083462Z - Title: B-1 (F029, operator): the `h` (headless start) / `s` (shortcut) keybinds fire from broad picker contexts (mod.rs handle_confirm_key / ChooseProject / Resume) regardless of whether the highlighted row would LAUNCH the endpoint. Restrict both to launch-capable highlights: (a) `Start now` in the immediate-start case (should_offer_project_choice == false), (b) a Choose-project row, (c) a Resume-from-history row. The footer hint line must render `h`/`s` ONLY when actually live (hint truth = availability truth). FIX: gate the key handlers on (screen, highlighted-option), unit the gate as a pure matrix. See triage B-1.
2026-09-06T13:09:01.9083571Z - Required stages: impl, unit
2026-09-06T13:09:01.9083600Z 
2026-09-06T13:09:01.9083702Z ### REQ-PICKER-CHANGE-ADAPTER-FLOW
2026-09-06T13:09:01.9086757Z - Title: B-2 (F029, operator; LARGEST item): `ConfirmOption::ChangeAdapter` wrongly routes into the CREATE flow (Screen::CreateAdapter → CreateId → CreateHome → START, reenter_create(true)) — it re-prompts id + home and then STARTS the endpoint. Required: prompt ONLY the harness-adapter pick, apply the change to the perch record (update info.json.adapter via the existing write seam; an `<adapter>:<profile>` pick stamps the full option, composing with A-4), then RETURN to the endpoint's Confirm menu — NO id prompt, NO home prompt, NO start. FIX: a return-to-Confirm mode on the adapter-pick screen (flag or dedicated Screen::ChangeAdapterPick) skipping CreateId/CreateHome + the launch outcome. Unit the flow-state transitions + the record write. Shared-seam: touches picker flow state — run the full picker cluster. See triage B-2.
2026-09-06T13:09:01.9087058Z - Required stages: impl, unit
2026-09-06T13:09:01.9087081Z 
2026-09-06T13:09:01.9087263Z ### REQ-TRANSLATE-COMMIT-MISS-TOLERANCE
2026-09-06T13:09:01.9090363Z - Title: C-1 (F029, B6 ROOT — rescope of REQ-TRANSLATE-BINARY-LIVENESS-DECAY, now PINNED): at a checkpoint clear boundary the clear-only inject drives `/clear`; its `{commit}` is never observed within INJECT_COMMIT_DEADLINE (5s, broker.rs:158) so the inject worker FAULTS + TERMINATES a HEALTHY translate binary (broker.rs respool_and_fault + return) and by ADR-0022 design NEVER respawns → every subsequent force-native reports delivered=false ('no live translation binary', cli.rs:5046) = B6's exact field signature; the v0.12.0 checkpoint post-clear WAKE dies with the terminated binary + the fire-and-forget FIRE in the dead window. NOT a race — deterministic at every checkpoint-armed boundary (perri captured-stderr proof: TRANSLATION_FAULT on the F-019 unread daemon-stderr channel). FIX (REVISED, supersedes terminate-then-respawn): miss != fault — preserve the binary; the watchdog's job is ANTI-STALL (release the operator floor), not execution-verification. See addendum C-1 + ADR-0022 amendment.
2026-09-06T13:09:01.9090750Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9090779Z 
2026-09-06T13:09:01.9090912Z ### REQ-HAZARD-TRANSLATE-FAULT-PERMANENT-DEATH
2026-09-06T13:09:01.9092830Z - Title: C-1 hazard (F029; paid-for: B6 + three-version checkpoint-wake breakage): a REAL translation fault (binary death — stdin write fail / stdout disconnect — or strike-budget exhaustion) must get a BOUNDED eager respawn (C3(b) give-up budget) instead of permanent death, and must stamp a PERCH-VISIBLE fault surface (mutate_info field, cleared on healthy respawn/commit) — TRANSLATION_FAULT is daemon-stderr-only today (the F-019 unread-channel trap; same honesty rule as F-027 ENDPOINT_SPAWN_FAIL). A real fault legitimately loses in-memory state (the wake is NOT carried across a real fault, unlike a mere commit-miss). See addendum C-1 (3)-(4).
2026-09-06T13:09:01.9092940Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9092969Z 
2026-09-06T13:09:01.9093078Z ### REQ-HAZARD-BOUNDARY-READY-STRAND
2026-09-06T13:09:01.9097660Z - Title: C-2 (F029, SEAM-2 pinned — B6's SECOND HALF, the live-wake blocker; perri wakep9 vs wakep4 gate-state dump + doyle code trace): at a /clear, CC fires SessionEnd(reason=clear) for the DEPARTING session BEFORE SessionStart; the departing sid STILL matches the perch pin at that instant, so the adapter's [hooks.SessionEnd] → `api session-end` AUTHENTICATES and the soft handler REMOVES the ready marker (+ unregister_address, reporting.rs cmd_session_end:206-207). The subsequent `api boundary` rotates the sid but NOTHING re-writes ready → is_online false → try_spt_hosted_inject Nones on the CLI gate BEFORE any broker RPC → every post-clear force-native (incl. the checkpoint FIRE) reports the generic UNDELIVERED, persistent by construction (no path re-stamps ready outside a real bind). The single differing gate field at every UNDELIVERED instant is ready-absent (info online/controllable/rotated-sid all healthy, translate alive). Paid-for hazard. FIX: cmd_boundary re-stamps the ready marker (+ status online, idempotent) ATOMICALLY with the sid rotation — a boundary PROVES a live successor session on the same harness process; a REAL end has no subsequent boundary so genuine teardown is untouched. See triage addendum C-2.
2026-09-06T13:09:01.9097828Z - Required stages: impl, int
2026-09-06T13:09:01.9097856Z 
2026-09-06T13:09:01.9097965Z ### REQ-PSYCHE-EPHEMERAL-DRIVER
2026-09-06T13:09:01.9101381Z - Title: W1 (F030, design §3): each psyche-relevant event runs exactly ONE bounded per-event turn through the existing driver stack (psyche_turn_and_relay for outbound-intent events / resume_psyche for session-custody transitions / run_psyche_turn for pure merges) — no resident psyche process exists between events. host_one (livehost.rs:518) STOPS spawning spawn_psyche_owned; the pulse loop stays as the daemon-side scheduler (thread + stop-flag + drop-dir watch correct) but a fire now invokes one bounded turn, daemon-driving every substitution key from daemon-known context (child never self-resolves home/subnet/perch — direction-(a) multi-subnet churn impossible by construction). Turn failures consume a bounded failure budget (C3(b) shape): N consecutive failures → psyche_host_error stamp + cooldown, reset on success; no respawn storm (nothing resident to respawn). Red-first: fire an event on a hosted live endpoint → assert one turn ran (SIDE-EFFECT PROOF FILE — transcript-jsonl asserts are structurally blind, 2026-07-04 rig lesson) and no {id}-psyche process survives the turn.
2026-09-06T13:09:01.9101705Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9101734Z 
2026-09-06T13:09:01.9101843Z ### REQ-PSYCHE-SID-CUSTODY
2026-09-06T13:09:01.9104817Z - Title: W2 (F030, design §3): the psyche mints and keeps its OWN session id, stored in the nested {id}-psyche perch record — {session_id} in psyche role templates becomes the psyche's sid, never the parent's (today's fill at livehost.rs:518 is the PARENT's — the custody bug). Parent boundary (/clear, /compact) does NOT rotate the psyche sid (the psyche's conversational thread survives parent resets — its job). resume_psyche validates the custody key before spawn (resume.rs:183). Reseed path: psyche session lost/invalid → ResumeMode::FreshWithPreload (download_psyche_context composes role/live/project into {psyche_context}, resume.rs:100) + LOUD PSYCHE_RESEED:{id} marker (custody-loss loop visible; W1 budget bounds it). If the parent sid is still needed by a template it gets its OWN explicit key {parent_session_id} — never aliased. Red-first: parent `api boundary clear` → nested perch sid UNCHANGED (today it is the parent's — guard-revert reproduces).
2026-09-06T13:09:01.9105110Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9105139Z 
2026-09-06T13:09:01.9105243Z ### REQ-PSYCHE-NESTED-RESOLUTION
2026-09-06T13:09:01.9109027Z - Title: W4 (F030, design §3; F-017 sibling, general not psyche-only): nested {parent}/{nested} ids resolve under the PARENT's home — subnet-less resolution for nested perches (the home-ASSIGNMENT seam, not perch-path which is already subnet-less) — so child-side verbs acting on nested perches need no --subnet the child cannot know. ROOT: home::assign_home returns Ambiguous on a multi-subnet node w/o --subnet; a nested id must instead derive home from its parent perch. Additionally expose a SINGLE {subnet} base_keys fill WHEN KNOWN (own_subnet = home-subnet label, 'local' when unhomed; absent → LOUD missing-key fail, the {node} precedent). NO {home} key (doyle W4 Q1: the endpoint home subnet is ONE concept per CONTEXT.md:640, and 'home:' is already the subnet-name qualifier position CONTEXT.md:652 — a {home} template key invites meaning-drift). Red-first = evidence #3's exact repro: 2-subnet home, nested-id verb, must succeed (was: `spt ready <id> --once` → exit 1 READY_FAIL … pass --subnet).
2026-09-06T13:09:01.9109222Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9109261Z 
2026-09-06T13:09:01.9109384Z ### REQ-PSYCHE-CONTEXT-FILE-INDIRECTION
2026-09-06T13:09:01.9113668Z - Title: W4 (F030; doyle Q2 ruling + perri file-always freeze, 2026-07-04): the composed psyche mind ({psyche_context}) rides the SHIM argv today — a real ~20KB doyle psyche-download exceeds the win32 CreateProcess lpCommandLine ~32k cap → the shim spawn BRICKS. FIX (file-always, replaces {psyche_context} outright — no size-branch, no argv cliff, one path): core writes the mind to a file in the psyche's NESTED perch dir BEFORE each turn spawn and fills a single {psyche_context_file} = that PATH (argv-cap-immune). The soft fresh/continue discriminator moves from KEY-presence to FILE-CONTENT: FreshWithPreload writes the composed mind NON-EMPTY (the <fresh-psyche/> never-empty guarantee carries to the file content); ContinueExisting writes it TRULY 0-BYTE (perri BINDING PIN 1 — NO sentinel/placeholder EVER, else her non-empty=fresh discriminator misfires a spurious --session-id adopt). Core owns the file lifecycle: write-before-spawn each turn, overwrite in place, persists between turns in the nested perch (debuggability); never deleted per turn. perri shim delta: --psyche-context-file <path> arg, read-file prefix, TRIM-based emptiness (her tolerance, NOT core's license — core writes exactly 0 bytes on continue, PIN 2), read-failure = generic fail NEVER 95, never writes/deletes the file. Red-first: a ~40KB mind → the old {psyche_context}-on-argv path BRICKS the win32 shim spawn; the file path succeeds (shim reads the full mind from file).
2026-09-06T13:09:01.9113969Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9113997Z 
2026-09-06T13:09:01.9114103Z ### REQ-PSYCHE-LEGACY-RESIDENT-SWEEP
2026-09-06T13:09:01.9120030Z - Title: W5 (F030; doyle+perri 2026-07-04): a dirty daemon upgrade from <=v0.24.0 strands a RESIDENT psyche wrapper the OLD daemon spawned — and F-030 W4's nested-`ready` resolution fix CONVERTED that wrapper's accidental self-reap into a permanent HANG. The pre-W3 wrapper's only spt IPC is `spt ready <parent>-psyche --once` (BLOCKING, no internal timeout); pre-W4 that hit READY_FAIL on a multi-subnet home → the wrapper exit-4'd (accidental reap). Post-W4 the nested id resolves cleanly → the wrapper REGISTERS then BLOCKS FOREVER on its first post-upgrade poll: no exit, no psyche_host_error, no CPU (KH 2.6 invisible-loop class, one level up). Post-W3 core has no residency machinery to reap it. FIX: a ONE-SHOT legacy-resident sweep at BRAIN START (never per-reconcile/periodic — burying residency-era machinery, not resurrecting it). GUARD = adapter-AGNOSTIC (glue-model): resurrect the retired reap_orphan_psyches LOGIC — for each self-perch live-agent id derive `<id>-psyche` and kill iff (a) exe basename == the adapter's MANIFEST-declared psyche program (normalize_basename, never a hardcoded adapter name) AND (b) cmdline contains the id marker `<id>-psyche` AND (c) pid alive; any unreadable signal → DECLINE + loud log (fail-safe-decline, positive-match-only; infra never-kill inside the sweep). FRATRICIDE is closed by TIMING (perri-confirmed from the owning side): the ephemeral shim is daemon-spawned per-event, bounded, exits at turn end — at brain start BEFORE the first reconcile/pulse no current shim is resident, so any `<id>-psyche` psyche-program process alive then is unambiguously stranded-legacy. RESIDUE (doyle PIN 3): the hung wrapper REGISTERED a `<parent>-psyche` ready perch before blocking; killing the pid alone leaves a phantom ready-record with a dead pid (the REMOTE-TRUTH presence-lie class) — the sweep MUST also clear that stale registration or prove the existing stale-perch cleanup reaps it. No field window pre-W6 (nothing releases). (F-030 W5)
2026-09-06T13:09:01.9120236Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9120268Z 
2026-09-06T13:09:01.9120383Z ### REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION
2026-09-06T13:09:01.9123274Z - Title: W3 (F030 hazard; paid-for: hall-bf churn ordinal 6491+ + adapter v0.13.2 bad-ship brick 2026-07-04): a psyche failure of ANY shape must NOT remove or alter the parent endpoint's ready/hosted state, and hosting must NOT churn-respawn. The v0.13.2 shim-exit tripped the residency machinery (confirm_residency_or_unhost) which tore down the endpoint's hosted state — ready marker removed, never re-stamped, every force-native gated leg=cli-gate-not-hosted PERMANENTLY (field brick). FIX: residency machinery retires with the resident child; the teardown that touches parent hosted state is DELETED — psyche trouble stamps psyche fields only. REQ-HAZARD-LIVEHOST-NONRESIDENT's spirit transfers to the W1 failure budget (its entry gets a SUPERSEDED pointer here, LIVENESS-DECAY→SUPERSEDED pattern from C-1). Conformance int = the hall-bf shape: multi-subnet home, live endpoint, failing psyche → parent stays deliverable, no rehost churn, error stamped (the wave's heart).
2026-09-06T13:09:01.9123390Z - Required stages: doc, impl, int
2026-09-06T13:09:01.9123418Z 
2026-09-06T13:09:01.9123517Z ### REQ-HAZARD-THRASH-GUARD-BLIND
2026-09-06T13:09:01.9124811Z - Title: W3 (F030 hazard; paid-for: evidence #6, hall-bf ~12/min re-host NEVER tripped the C3(b) thrash guard — boot records were not ledger boundaries to the guard): the failure budget must count REAL attempts (ledger-derived: boot/turn records via the psyche perch ledger), not whatever it counted that let 12/min churn run invisibly. Red-first synthetic loop: a 12/min synthetic failure loop MUST trip the budget.
2026-09-06T13:09:01.9124916Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.9124940Z 
2026-09-06T13:09:01.9125040Z ### REQ-EFFECTIVE-INSTANCE-STATE
2026-09-06T13:09:01.9129261Z - Title: A-1 (REMOTE-TRUTH triage §A + ADR-0033 §Decision): the effective instance state of a perch is DERIVED through ONE shared function — liveness discriminates warm/cold, stored rest intent refines within warm, absent intent NEVER defaults active. ROOT (certain): resting::apply_event derived its `from` off the stored rest_state field ALONE (resting.rs:225, `unwrap_or(RestState::Active)`) — a cold perch (offline) with no intent answered `from=Active`, so a Wake event found it 'already in target state' and returned Ok(None) = the field NO_EDGE-on-a-definitely-suspended-endpoint bug (the banked F-028 rest_state-void seed). advertised_status (registryhost.rs:821) ALREADY derived correctly (is_perch_alive→intent-refined / is_perch_unbound→Dormant / cold→Suspended) — the two readers disagreed. FIX (Q1 shared derivation, hazard-class): a pure `effective_rest_state(alive, unbound, intent) -> RestState` mirroring advertised_status, consumed by BOTH advertised_status (mapped RestState→Status, behavior identical) AND apply_event's `from` (real is_perch_alive/is_perch_unbound reads); void + cold ⇒ Suspended. Bonus: kills the spurious active→suspend echo a cold+void perch used to fire (on_rest_edge on a dead driver). Red-first: perch status=offline + no rest_state → daemon_rest_event(Wake) yields from=Suspended→to=Active EdgeReport, not Ok(None).
2026-09-06T13:09:01.9129456Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.9129490Z 
2026-09-06T13:09:01.9129594Z ### REQ-OPID-MINTER-NAMESPACE
2026-09-06T13:09:01.9136358Z - Title: A-4a (REMOTE-TRUTH triage §A + ADR-0034 Decision 1 + Amendments 1 & 2): the broker effect journal's dedup key gains a minter dimension so ops minted by independent counters can never collide. ROOT (high, ground-truthed vs HEAD): the journaled-op producers key into ONE journal namespace (NET_EFFECT_SESSION|shell_sid, op) at broker.rs (EffectKey=(u64,u64)); a CLI wake op colliding with an already-journaled daemon op reproduces the typed 'already applied … retry with a fresh op_id' with NO broker restart (field-hit: spt endpoint wake id@node WOKE_FAIL). Same latent class: nethost dial_ops/stream_ops HashMap<u64,u64> ('Shares the one net op-id namespace') would re-clobber even after the journal separates them; AND shellchan::deliver_stdin_pending journals (shell_sid, row_id) so an rc operator's ops on the same shell_sid collide with spool row ids (dropped keystroke OR dropped spool row). Amendment 2 corrected the minter set: the REAL journal minters are {cli, pump, rc, shell} + legacy — psyche/epoch are the EpochSource notif/lease counter domain, NEVER submit to apply_once, DROPPED from the journal enum (a tag with no stamp site = doc'd-but-dead knob). FIX (Decision 1 + Amdt 2): ONE canonical Minter enum {Legacy, Cli, Pump, Rc, Shell, Wake} — Legacy reserved for pre-upgrade lines + untagged wire, monotonically shrinks; enum is the single source for the TEXTUAL journal-line token (self-describing during recovery). EffectKey becomes (effect-class, minter, op); recover() DUAL-PARSES (old shorter line → minter=Legacy, new longer line → parsed tag) so old journals need no migration and old-shape keys can never equal new-shape (migration-free). A MintedOp{minter, seq} newtype REPLACES bare op_id:u64 through the brain/daemon THREADING paths so forgot-to-stamp is UNCOMPILABLE (row_id stays the shell seq — never re-minted, the durable spool exactly-once identity). Wire keeps an additive optional minter field (serde default absent ⇒ Legacy materialized at broker decode; serde_json no deny_unknown_fields ⇒ NO wire version bump); the newtype is NOT forced into wire structs. nethost op-maps re-key by (minter, op). Red-first: mint an rc op == a journaled shell/pump/daemon op int on the same session → pre-fix the second dedups/clobbers (WOKE_FAIL class); post-fix both are distinct keys, both Applied.
2026-09-06T13:09:01.9136545Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.9136573Z 
2026-09-06T13:09:01.9136672Z ### REQ-OPID-TRACING-RETRY
2026-09-06T13:09:01.9141217Z - Title: A-4b (REMOTE-TRUTH triage §A + ADR-0034 Decision 2): the tracing-only op families auto-retry ONCE with a fresh op on the typed no-longer-held error, so a broker-restart-dropped conn/stream self-heals instead of surfacing core lingo to the user. ROOT: net/rest.rs declares rest op-ids 'tracing/correlation only … redelivery needs reporting, not dedup', yet the journal enforced exactly-once on them — a rest/attach op whose conn the broker no longer holds (post-restart) returned the typed 'already applied … no longer held … retry with a fresh op_id' (broker.rs:2796 net-dial / 3047 stream-open) straight to the user (WOKE_FAIL / rc attach fail). FIX (Decision 2, scoped): the rest family (request_rest) + rc attach stream-open (request_attach_endpoint) — and ONLY those — catch the typed no-longer-held error INTERNALLY and re-issue ONCE with a FRESH op minted from the SAME minter (A-4a MintedOp; same producer, new seq). sync/update pull families (request_sync/request_update, durable open_op) are NOT wrapped — their exactly-once dedup is load-bearing (negative control). The typed op error becomes internal-only; if the retry ALSO fails, the user-facing line names the observable situation + next action in operator language, ZERO journal/op/brain lingo (F-1 public-error rule applies early — this string is user-facing). Red-first: mint a colliding op → assert the retry succeeds + the user sees NOTHING; NEGATIVE CONTROL — a durable family's no-longer-held stays a hard error, no silent retry.
2026-09-06T13:09:01.9141652Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.9141680Z 
2026-09-06T13:09:01.9141788Z ### REQ-REST-VERB-ROUTING
2026-09-06T13:09:01.9149857Z - Title: A-3 (REMOTE-TRUTH triage §A + Q3 operator-law): a BARE-id rest verb (spt wake/suspend <id>) routes across the subnet like send's fallback instead of failing local-only. ROOT (certain): cmd_rest (cli.rs:3296) gates the remote arm on id.contains('@'|':'); a bare id falls to the local-only arm (cli.rs:3340) → daemon_rest_event → info::read_info miss (resting.rs:248) → 'WOKE_FAIL:{id}: info.json absent or unreadable — not a hosted perch'. cmd_send (cli.rs:5142) DOES fall back on a local miss; cmd_rest's remote arm (cli.rs:3307, wan_rest) already handles every WanRestOutcome — it is simply never reached on a bare-id local miss. Contradicts CONTEXT:286 'a wake must route'. Q3 SUBSTRATE GAP: resolve_across_visible (registry.rs:971) filters only by Status::routable() and its Ambiguity payload is node-hexes-only — it CANNOT express the Q3 status rule; per-candidate (node,status) comes from SubnetRegistry::instances(id). FIX: a NEW pure select_rest_target helper (status-aware, isolated from resolve_across_visible which cmd_send keeps) applying GOAL-SATISFACTION semantics (ADR/triage addendum @188d269, NOT naive verb symmetry — the mixed case breaks symmetry): wake is an ∃-goal (satisfied when ANY instance Active), suspend is a ∀-goal (satisfied when ALL instances Suspended); one helper parameterized by the verb's satisfaction predicate — 0 candidates→NotFound; goal already satisfied→NoOp naming the satisfying node(s); exactly 1 ACTIONABLE (not-at-target) instance→Act(node); >1 actionable→Ambiguous(copy-paste id@node list). Edge rulings: wake with >1 Active = NoOp naming ALL active nodes (NOT Ambiguous — nothing actionable); suspend mixed (X suspended + Y active, NOT ∀-satisfied) = Act(Y) if exactly one active / Ambiguous if several active. Candidate status is ADVERTISED/gossiped (post-A-1 shared-derivation, may be STALE) so a NoOp verdict is ADVISORY and the qualified id@node path is the operator override (noted in the helper doc-comment). cmd_rest's bare-id local miss loads snapshots → instances(id) → select_rest_target → dispatches (Act→wan_rest to the node / NoOp naming node(s) / Ambiguous render_refusal copy-paste id@node list / NotFound NO_ENDPOINT), all F-1 public language from day one. Qualified id@node path unchanged; shutdown leg-2 stays LOCAL_ONLY. Red-first: a bare id present ONLY in a remote registry snapshot routes to that node instead of WOKE_FAIL.
2026-09-06T13:09:01.9150218Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9150252Z 
2026-09-06T13:09:01.9150353Z ### REQ-SESSION-ADAPTER-RECORDED
2026-09-06T13:09:01.9155017Z - Title: D-2 (REMOTE-TRUTH triage §D-2 + operator Q5 @c248afc): the session ledger records the adapter[:profile] a session ran under, so a later resume can restore the harness the session actually used (not merely the endpoint's CURRENT stamp). ROOT: SessionEntry (spt-store/sessions.rs:58) carries ts/session_id/trigger/cwd/ordinal but NOT the adapter — a resume-from-history row cannot know which harness authored the transcript, so a resume under a since-changed endpoint adapter (B-2 ChangeAdapter, or a fork) launches the wrong harness. FIX: an ADDITIVE `adapter: Option<String>` on SessionEntry, exact cwd/ordinal serde pattern (#[serde(default, skip_serializing_if="Option::is_none")]) — a pre-migration row missing the key deserializes None; None omits the key on serialize (byte-identical to old rows); an unknown key on an old reader is ignored (serde default) — back-compat BOTH directions. Stamped at every PRODUCTION session-boundary append. CENSUS (doyle-confirmed @94f0205, corrects the triage-era 5-site drift to the real 3): startup.rs:317 (live bind boot row, rec.adapter in scope), reporting.rs:94 (boundary rotation row UNDER the mutate_info lock, capture adapter_for_ledger=rec.adapter beside cwd_for_ledger), ready.rs:119 in crates/spt-msg (ready-agent boot row, rec.adapter in scope). NOT digest.rs:601 (cfg(test) fixture) and NOT a livehost psyche-ledger append (none exists — the live /clear|/compact boundary shells `api boundary` → reporting.rs:94, the SAME append). None-stamp is a benign degrade (resume falls back to the endpoint's current adapter).
2026-09-06T13:09:01.9155197Z - Required stages: impl, unit
2026-09-06T13:09:01.9155225Z 
2026-09-06T13:09:01.9155335Z ### REQ-RESUME-ADAPTER-FOLLOWS-SESSION
2026-09-06T13:09:01.9161636Z - Title: D-2 (REMOTE-TRUTH triage §D-2 + operator Q5 @c248afc): a resume-from-history restores the RECORDED session adapter (REQ-SESSION-ADAPTER-RECORDED) — the resumed harness is the one the session ran under, re-stamped onto the endpoint PRE-SPAWN, and an unregistered recorded adapter refuses LOUDLY before launching anything. ROOT: the picker's resume_outcome (model.rs:1285) bakes adapter=ep.adapter_profile from the selected ENDPOINT, ignoring the ledger row — so a resume always uses the endpoint's CURRENT adapter even when the session ran under a different one; and the endpoint's info.adapter is never re-stamped to the row's on the resume path (cli.rs:1962 skeleton writer early-returns for an existing perch — adapter immutable, carried by bind's stamp_creation_fields). FIX (doyle fork ruling): ResumeRow (model.rs:199) gains adapter: Option<String> threaded from SessionEntry.adapter in picker/data.rs; the row title (model.rs:228) renders [{adapter}] when Some ({head} [{adapter}] - {time} (…{id5})); resume_outcome bakes the ROW's adapter with an endpoint fallback (row.adapter.unwrap_or(ep.adapter_profile)) — None → the endpoint's current stamp (benign degrade). The pre-spawn RE-STAMP + refusal ride the picker resume dispatch (mod.rs:360 Run arm, resume.is_some()) reusing the hazard-guarded mutate_info seam (write_adapter_change/mod.rs:336), NEVER the bind path: order = read current info.adapter → if the baked adapter DIFFERS (a real replace; a None-row bakes the endpoint's own → equals current → NO write) → registered-check via resolve_option (Err(NotRegistered) → loud F-1 refusal naming the adapter + `spt adapter add`, NO stamp, NO spawn) → write_adapter_change re-stamp → spawn. ONE adapter write path (the mutate_info seam); REQ-HAZARD-ADAPTER-PROFILE-STAMP-CLOBBER's bind/hook path (stamp_creation_fields, home.rs) UNTOUCHED — both its guard tests stay green as the gate condition. Red-first: a resume row adapter="claude-spt" over an endpoint stamped "claude-spt:ccs" → the baked Outcome.adapter == "claude-spt" (the deliberate replace) and the pre-spawn stamp writes it.
2026-09-06T13:09:01.9161832Z - Required stages: impl, unit
2026-09-06T13:09:01.9161956Z 
2026-09-06T13:09:01.9162056Z ### REQ-WAKE-RESUME-LEG
2026-09-06T13:09:01.9167868Z - Title: A-2 (REMOTE-TRUTH triage §A-2 + ADR-0033): the daemon reconcile gains a WAKE-RESUME LEG — an endpoint whose rest INTENT is Active but whose harness session is COLD (status != online) is resumed by the daemon via the adapter's [session.resume] template using the LAST LEDGER session id, so a bare `spt wake <id>` on a suspended live agent actually brings it back (today: reconcile_once start-arm hosts ONLY status==online (livehost.rs:199), so a woken-but-unbound endpoint is skipped forever — neither status reaches online nor does reconcile re-host). This is the ADR-0033 LIFT: the thin `spt wake` edge writes rest intent, the DAEMON does the work. Mirrors shellwake::resolve_wake (read rest state, live-pid double-launch guard, launch, NEVER flip status — the harness self-binds → online). The leg reads the recorded adapter (D-2, REQ-SESSION-ADAPTER-RECORDED); an UNREGISTERED recorded adapter is the Q5 daemon-variant refuse: do NOT spawn, record a LOUD host_error report (F-1 naming the adapter + `spt adapter add`), never silent, never fallback-spawn on a different adapter. BINDS: (1) status=online is set ONLY by a real bind — the resume leg NEVER stamps it (CONTEXT liveness truth; the A-1 effective-state derivation depends on this staying honest). (2) host_error is a REPORT of the most recent host-level failure, NEVER a liveness input — neither liveness nor advertised_status reads it (host_error + online still derives Active); cleared on a successful host/bind; the existing silent `continue` on a deregistered online adapter (livehost.rs:205) folds into the same field. (3) the resume-pid guard marker is CUSTODY-ONLY (F-030 nested-record discipline) — never a liveness input. cold-with-no-ledger-row degrades benign (loud-logged skip, no crash, today's behavior). Single-node; C-2 picker Wake-now unblocks after. --wait is a SEPARATE rider (REQ-WAKE-WAIT).
2026-09-06T13:09:01.9168091Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9168125Z 
2026-09-06T13:09:01.9168216Z ### REQ-WAKE-WAIT
2026-09-06T13:09:01.9170101Z - Title: A-2 rider (REMOTE-TRUTH triage §A-2): `spt endpoint wake --wait` blocks on the REAL bind (status reaches online) after the daemon wake-resume lift (REQ-WAKE-RESUME-LEG), instead of the DEFAULT accepted-not-bound print (thin edge writes intent, daemon lifts async — ADR-0033). Reuses the F-027 bind-await machinery if/when it lands, else a bounded poll on status==online with a plain-language timeout (no core lingo, F-1). Default wake is UNCHANGED (accepted-not-bound truth). Separate chunk from the core leg (doyle A-2 ruling: C-2 needs the core leg, not --wait); F-027 bind-await stays design-only until this activates.
2026-09-06T13:09:01.9170204Z - Required stages: 
2026-09-06T13:09:01.9170229Z 
2026-09-06T13:09:01.9170333Z ### REQ-PICKER-REMOTE-WAKE
2026-09-06T13:09:01.9176729Z - Title: C-2 (REMOTE-TRUTH triage §C-2 #4 + addendum @3442ce5): a REMOTE suspended picker row offers `Wake now` — waking the endpoint THROUGH its owning node's rest edge (the A-2 daemon resume leg) — instead of a bare `Start now` that silently cold-starts a COLLIDING LOCAL instance of a remote id (node-anchored identity violation, ADR-0003/0023). ROOT (certain): confirm_options collapsed Suspended into the offline action set = [Start,…]; on a REMOTE row `Start` bakes Outcome::Run with NO node → picker dispatch → cmd_endpoint_run creates a fresh LOCAL perch of the remote id (model.rs confirm_terminal / mod.rs dispatch). Remote rows are only Online/Suspended, so remote+offline == remote-suspended. FIX: confirm_options splits the offline arm on is_local — remote → vec![Wake] (a new ConfirmOption::Wake), local → vec![Start] UNCHANGED; confirm_terminal(Wake) → a new Outcome::Wake{id,node} carrying the RAW node hex; dispatch routes crate::cli::cmd_endpoint_wake_remote(id,node) → cmd_rest(id@node, RestEvent::Wake) = the EXISTING WAN rest arm (dispatch_wan_rest → wan_rest), and A-2's resume leg revives the session async (the full loop the operator wanted). `Instantiate locally` stays the separate deliberate-copy verb. ADDENDUM correction (a): EndpointRow.node is the LOSSY DISPLAY string (node_label_display = 'LABEL (prefix…)'), which node_qualifier_matches (full-hex-prefix|exact-label) CANNOT match — a dead Wake; so a NEW EndpointRow.node_key: String carries the raw ResourceRow.node hex (empty for local rows — Wake is remote-only) threaded through from_resource_row + the 4 literal ctors. CO-GATE (b, addendum): ChangeAdapter (was `offline`-gated) is gated `offline && is_local` — write_adapter_change → resolve_perch_path(Infer) → mutate_info rewrites a LOCAL perch record, so offering it on a remote suspended row is the SAME colliding-local-write-for-a-remote-id class (the Start twin); a remote node's adapter is not ours to rewrite from here. Red-first: a remote suspended row → confirm_options has Wake NOT Start NOT ChangeAdapter, and confirm_terminal(Wake) → Outcome::Wake{node==raw hex} (never a local Outcome::Run); a LOCAL offline row is UNCHANGED (Start + ChangeAdapter).
2026-09-06T13:09:01.9177058Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9177087Z 
2026-09-06T13:09:01.9177198Z ### REQ-HAZARD-BROKER-FLOOR-LOCK-POISON
2026-09-06T13:09:01.9184876Z - Title: B-1 (REMOTE-TRUTH triage §B-1, PIVOTED @e5eb99a): NO bare `.lock().unwrap()` on a broker-resident lock reachable from serve/dispatch — a brain-only self-update keeps the broker + all its Mutexes ALIVE (REQ-UPD-3), so a single panic under one poisons it PERMANENTLY: the next `.lock().unwrap()` panics, kills its per-conn reply thread, and EVERY subsequent attach silently deadlines ('brain IPC read deadline elapsed') while non-locked ops keep working. TRIAGE-DRIFT (sweep-dispatch-site-counts discipline): the triage named 3 sites (broker.rs:1163 flush_inject_floor / :1297 inject-worker-open / :2142 buffer_if_held) as the surviving class, but ALL 3 are the INJECT FLOOR and were ALREADY poison-proofed by REQ-HAZARD-INJECT-WORKER-POISON (lock_floor, shipped post-triage — the FLOOR HALF is SUBSUMED, this seed redirects). The SURVIVING class (matching the triage's own symptom description) is the ATTACH-PATH lock set: self.sessions Mutex<HashMap> ×18 + its sessions_exit alias ×1, the per-session OutputLog RING ×11 (log/h.log/log_drain/log_exit), pair_holds ×4 — 34 production bare .lock().unwrap() (cfg(test) excluded). FIX (doyle B-1 ruling): recover ALL THREE via ONE shared `recover<T>(&Mutex<T>) -> MutexGuard<T>` helper (into_inner idiom, same as lock_floor / the effect journal bug #16 — safe for the short coherent-on-recovery map ops of sessions/pair_holds), plus `recover_log(&Mutex<OutputLog>)` for the ring which adds a COHERENCE CLAMP on the poison-recovery path: a panic mid-append can leave the ring torn (over-cap, a last seq not below next_seq, non-monotonic front/back) and serving those bytes risks garbage, so OutputLog::clamp_or_reset cheap-checks the invariants and RESETS the ring empty (next_seq preserved — cursors never rewind) + loud-logs on violation. Rationale: fail-fast on the log reintroduces the very wedge B-1 kills (poisoned log = every subscriber attach panics forever); blind recover serves torn bytes; clamp-or-reset costs only scrollback that self-heals on the next PTY output + repaint (lost scrollback << permanent wedge, torn-serve eliminated not tolerated). Sessions/pair_holds recover bare (short map ops, coherent-on-recovery). CLASS invariant (KNOWN-HAZARDS 7.33): any new broker-resident lock uses recover/recover_log or a documented fail-fast justification. Red-first: a scripted panic-under-sessions-lock → recover hands back a usable guard, the next attach still opens; a TORN-RING variant → recover_log clamps/resets so the subscriber gets sane bytes.
2026-09-06T13:09:01.9185075Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.9185100Z 
2026-09-06T13:09:01.9185205Z ### REQ-CONTROLLER-LIVENESS-REAP
2026-09-06T13:09:01.9193379Z - Title: B-2 (REMOTE-TRUTH triage §B-2, REDUCED @bdc1242): a stale ONLINE+CONTROLLED stamp on a live-session perch self-heals — the info.json driven_by/controlled RECORD is made to match the broker's SINK-TABLE TRUTH. ROOT (persisted-stale-stamp class, doyle Q1): the livehost control reap gates on !has_session (reconcile_hosted_liveness), and a brain-only update KEEPS the session (REQ-UPD-3), so a controller stamp that went stale WHILE the session lived was never re-derived from broker truth. NOT a transport bug: (a) a persisted conn is the REQ-UPD-3 feature; (b) an idle-severed conn eventually EOFs via QUIC keepalive → handle_conn detach (path 1) — and the reason paths 1-3 previously failed to clean up was the B-1 broker floor-lock POISON WEDGE (cleanup panicked under the poisoned lock), now fixed. REDUCTION (doyle, my ground-truth): the prescription was 80% pre-built — converge_perch_stamps (broker.rs, REQ-HAZARD-CONTROL-STAMP-CONVERGENCE) ALREADY converges info.json driven_by/controlled to the broker's controller_by/has_controller on EVERY KIND_SESSIONS poll, and the livehost reconcile already TRIGGERS that poll per tick (query_live_session_endpoints). So NO new IPC query, NO new livehost arm, NO 5th detach path — the ONLY gap is that a controller whose WRITER THREAD died (severed conn: the writer failed a socket write, or a detach dropped by the prior B-1 wedge) still reports controller_by=Some/has_controller=true, so converge keeps the stale stamp. FIX: a broker-side lazy-reap in the KIND_SESSIONS snapshot closure — OutputLog::reap_dead_controller() drops a controller whose _writer.is_finished() BEFORE controller_by/has_controller are read, so the reply + the off-lock converge both see the honest (cleared) state and the stamp clears. LOCK-SAFE: the reap drops the sink in-memory ONLY (no stamp_driven_by → no info.json I/O under the log lock, the KH 7.12/5.16 lock-across-effect discipline); the OFF-lock converge_perch_stamps writes the honest stamp. KH 7.15 held by construction: the reap only ever CLEARS, never latches driven_by; a LIVE (idle, parked-on-rx.recv) controller is is_finished()==false so it is NEVER false-reaped. RESIDUAL (doyle Q2 accepted): a TRULY IDLE severed controller (writer parked on recv, no output, conn not yet EOF'd) stays is_finished()==false and converges only on output-resume / conn-EOF — that harder active-probe case is the RESERVED REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT (SessionInfo.controller_by doc), deliberately NOT built here so the reserved seed keeps its scope. Red-first: a dead-writer sink → reap clears it (controller_by honest None → converge clears the stamp); a live-writer sink → UNTOUCHED (no-false-reap control).
2026-09-06T13:09:01.9193664Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9193689Z 
2026-09-06T13:09:01.9193785Z ### REQ-ADAPTER-FLOOR-ENFORCE
2026-09-06T13:09:01.9203759Z - Title: F-5 (REMOTE-TRUTH triage §F-5 + doyle rulings 2026-07-05): BOTH adapter acquisition verbs (spt adapter add + spt adapter update) REFUSE when the installed spt-core is BELOW the adapter's declared [adapter].min_spt_core_version floor — with an F-1 operator refusal naming the installed core, the floor, and the next action (update spt-core first). ROOT: the floor was PARSED + required (manifest.rs) but never compared to the running core — dead enforcement; and the [update].version_check knob that gated it was DOC'D-BUT-DEAD (never read by any production path — a contract lie). RULINGS: RETIRE version_check (drop the manifest field + schema + docs + the cfg(test) literals; a pre-existing manifest still setting it deserializes fine — serde ignores the unknown key, no deny_unknown_fields, so retiring is back-compatible); SEMVER-compare NOT string-compare (the 0.9.0 < 0.25.0 lexical trap); enforce on BOTH verbs; nothing installs / registry untouched on refuse (binds both verbs, no residuals). FIX: (1) a pure spt-runtime version_meets_floor(core, floor) -> bool (numeric per-component: split '.', u64, missing→0, non-numeric→0, first-diff decides, equal-when-zero-padded ⇒ satisfied) — mirrors the CLI version_is_newer parse (same numeric model, different question: freshness=strictly-newer vs floor=at-least). (2) ADD: the gate lives INSIDE registry::register (the choke point) via a register_with_core(core_version) seam register() delegates to with env!(CARGO_PKG_VERSION) — the floor check runs right after the manifest parse, BEFORE any registry write, returning the typed RegistryError::CoreFloor{adapter,core,floor} (Display = the ONE F-1 refusal both verbs surface); nothing recorded on refuse. (3) UPDATE: a PRE-SWAP peek (staged_floor_ok) extracts the staged .spt to a THROWAWAY temp, parses its manifest floor, and refuses BEFORE apply_release_crc_swap mutates the live pointer-mode home — so a refusal (or an unverifiable floor: FAIL-CLOSED) leaves the live install BYTE-UNTOUCHED; register@8932 stays as the defense-in-depth backstop for every other entry path. THE COMPARISON BASIS IS THE CALLER'S, NOT THIS FUNCTION'S (releases#159): the bare verb judges against the running CLI, and a COMPOSITE update judges against the core that run will activate — see REQ-ADAPTER-FLOOR-VS-STAGED-CORE. The FAIL-CLOSED discipline in this clause is about an unverifiable STAGED MANIFEST and is unrelated to that input. doyle bind: the register-only gate would let the crc-swap replace the live files with a floor-violating version while the record refuses (record and reality disagree — the exact contract-lie shape this milestone kills), so the pre-swap peek is the only correct answer. Red-first: perri negative repro on ADD (fresh home + synthetic low core + high-floor manifest → CoreFloor refuse, registry untouched) + the UPDATE pre-swap refuse (live home byte-untouched) + a floor-met positive control (0.25.0-on-0.25.0 installs); + version_meets_floor table incl. the 0.9<0.25 trap.
2026-09-06T13:09:01.9204383Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.9204412Z 
2026-09-06T13:09:01.9204573Z ### REQ-ADAPTER-FLOOR-VS-STAGED-CORE
2026-09-06T13:09:01.9226227Z - Title: IN A COMPOSITE UPDATE, AN ADAPTER'S CORE-VERSION FLOOR IS JUDGED AGAINST THE CORE THAT RUN WILL ACTIVATE, NOT THE ONE CURRENTLY RUNNING (releases#159, doyle ruling 2026-08-21). THIS DELIVERS A CONTRACT ALREADY RULED AND NEVER SATISFIED, which is what the lane is FOR: CONTEXT.md's adapter-manifest-header entry has said since the glossary was written that `when spt-core self-updates, re-verify adapters still satisfy (coordinate core + adapter updates when needed)` — the code never did. Same shape as F-5 itself (REQ-ADAPTER-FLOOR-ENFORCE), where the floor was PARSED and REQUIRED and never compared: a dead rule, not a missing one. THE DEFECT: `spt update` plans FetchApply -> Adapters, and `spt update --restart` plans Fetch -> Adapters -> Finish; in BOTH the adapters leg runs while the NEW core is staged and the invoking CLI is still the OLD image, so a floor comparison against `env!(CARGO_PKG_VERSION)` asks a question about a core that is on its way out. An adapter whose floor the INCOMING core satisfies is therefore refused in-cycle, and the run then activates that very core and leaves the OLD adapter sitting on it — the refusal protected nothing, it deferred the pairing it was meant to enforce, ungated, into a state nobody checks again. FIX: `cmd_adapter_update` takes the comparison version as a PARAMETER instead of baking `env!`; the composite leg passes the STAGED release's `product_version`, read from the VERIFIED signed metadata via `ReleaseCache::staged_update` (`UpdateSetMetadata` / `ReleaseMetadata`). SINGLE-PASS, NOT A RE-SWEEP (doyle): a post-finish second sweep can itself fail half-rolled, so the one pass judges against the truth it already holds — and the CONTEXT.md clause above is amended to state that cadence, because a glossary sentence whose cadence a future implementer could build against would authorize the refused re-sweep with the glossary's own authority behind it. BOTH COMPARISON SITES INSIDE THAT FUNCTION TAKE THE PARAMETER (doyle confirmation 2026-08-22, dated to the confirmation and not to the lane: the correction postdates the ruled shape it corrects) — (a) the pre-swap peek `staged_floor_ok`, and (b) the post-swap re-register, which moves from `registry::register` (the `env!`-baking wrapper) to the already-public `registry::register_with_core` seam beneath it. (b) IS LOAD-BEARING, NOT TIDINESS: fixing only (a) admits the adapter at the peek, crc-swaps the new bits into the live pointer-mode home, and THEN refuses at the record against the old running core — live files new, registry record old, reported as FAILED. That is the record-and-reality disagreement REQ-ADAPTER-FLOOR-ENFORCE's own pre-swap-peek bind exists to prevent, reached from the other side, and it is strictly worse than the bug this requirement fixes (today's refusal is clean and byte-untouched). One value, two consumers, SO THE PEEK AND THE RECORD CANNOT DISAGREE ABOUT WHICH CORE THEY JUDGED AGAINST — that property is what makes this a fix rather than a second gate to keep in sync, and it is why the parameter is THREADED rather than each site reading the staged metadata for itself. THE BARE VERB IS UNCHANGED: `spt adapter update` (cli.rs AdapterCmd::Update) and its `spt update adapters` alias keep passing the running CLI's version — with no update in flight the installed core IS the right question — and both call sites are pinned as unchanged. `spt adapter add` is untouched: no staged core exists on that path. FAIL OPEN, ON THE NEW INPUT ONLY (doyle): an unreadable staged version degrades LOUDLY to the running-CLI comparison — an absent answer is not a zero, and an empty string is never compared against a floor — while `staged_floor_ok`'s deliberate FAIL-CLOSED arm for an unverifiable STAGED MANIFEST floor (REQ-ADAPTER-FLOOR-ENFORCE) is NOT touched. ONE ARM, THREE DIAGNOSTICS (doyle Q1 ruling, extended 2026-08-22 by measurement): an absent stage, an empty `product_version` (`#[serde(default)]` on both metadata types — pre-v0.3.2 releases really do parse to an empty string), and metadata that parses as NEITHER type all take the SAME degrade to the running core but must NOT share a message, because the operator's next step differs: nothing to do, an old release, and inspect your stage. The third states the FACT and points AT the stage without diagnosing it, borrowing 'malformed' from the apply leg's own vocabulary so two arms of one run never name the same corrupt artifact differently; the second offers predating-the-field as the LIKELY cause rather than the certain one, since any producer omitting the key yields the identical empty string. MERGING THE THIRD INTO THE SECOND IS SPECIFICALLY FORBIDDEN: telling an operator their release predates a field when the artifact is DAMAGED is a benign explanation offered for a corruption condition. THE THIRD ARM IS DEFENCE IN DEPTH AGAINST A STATE THE RUN SHOULD ALREADY HAVE REFUSED, and this is recorded here because the arm will otherwise look like dead code to the next reader and be deleted: in a COMPOSITE it is almost unreachable BY CONSTRUCTION — the core legs reject a malformed stage as a FAULT (RejectReason::Malformed, UPDATE_APPLY_REJECTED, exit 1 and not 3, since an integrity failure is not a policy decline) and composite_abort_on_failure is true for BOTH FetchApply and Fetch, so the run aborts before an adapters leg exists. It is reached only when a core leg exited 0 over an already-corrupt stage (a fetch with nothing new to pull) or when the stage changed after that leg ran, which is why its sentence names both possibilities and asserts neither. MEASURED REACHABILITY, one door only (relcache.rs:330-351): corruption at the SignedRelease envelope dies at `.ok()?` and yields None, indistinguishable from no stage and correctly reported as such — only inner metadata that parses as neither type, WITH an artifact on disk, falls to the `else if` and reaches the seam as Some. READING `product_version` HERE DOES NOT VIOLATE ITS OWN WARNING, stated in this title so the next reader meets the warning before they meet the use: that field's doc forbids using it to decide WHICH RELEASE IS NEWER — the monotonic `version` counter is the ordering authority and this requirement does not touch ordering — it does not forbid reading it as the semver a declared floor is compared against, provided it comes from the verified signed metadata, which is the only place this reads it from. Gate: doc — the CONTEXT.md adapter-manifest-header clause restated as the ruled cadence (the sentence this requirement delivers), the CONTEXT.md update-composite entry's floor-gate phrase, and MANIFEST.md's enforcement note, each naming WHICH core the floor is judged against; impl — the comparison-version parameter on `cmd_adapter_update` threaded to both the peek and `register_with_core`, the composite leg's staged-version read off the verified metadata, and the two-diagnostic loud degrade; unit — an adapter flooring on the FETCHED core is ADMITTED by the composite AND its record registers at the new version, the bare verb and its alias still compare against the running CLI, and the empty-`product_version` and absent-stage degrades each render their own diagnostic and fall back rather than refusing.
2026-09-06T13:09:01.9226826Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.9226859Z 
2026-09-06T13:09:01.9226963Z ### REQ-RUN-ID-REUSES-ADAPTER
2026-09-06T13:09:01.9231253Z - Title: D-1 (REMOTE-TRUTH triage §D-1): `spt endpoint run --id <id>` with NO --adapter, when <id> names an EXISTING perch, REUSES that perch's recorded info.adapter and runs NON-INTERACTIVELY — instead of always falling to the picker as a create-new prefill (an existing endpoint retyping its own adapter, or being sent to a create-new flow, is the operator wart). ROOT (certain, no design tension): the cli `match (adapter,id)` special-cased only (Some,Some)→cmd_endpoint_run; the catch-all routed EVERY lone --id to crate::picker::run as a create-new prefill, never considering an existing endpoint (cli.rs ~1290). FIX: a PURE resolve_run_target(adapter, id, recorded) over the 4 (adapter?,id?) quadrants — (Some,Some)→Direct{a,id}; (None,Some(id))→ recorded adapter present (info.adapter = adapter-chosen-at-creation, spt-store info.rs:167) → Direct{recorded,id}, absent/no-perch → Picker{None,Some(id)} (today's create-new prefill UNCHANGED); (Some,None)/(None,None)→Picker unchanged. The perch lookup (read_info(resolve_perch_path(id,Infer)).adapter) is INJECTED as a closure so the router is pure + testable without a perch on disk; resume threads into BOTH Direct paths. Red-first: (None,Some(id),recorded=Some) → Direct (pre-fix this routed to Picker create-new). int (live run --id on an existing perch reuses its recorded adapter non-interactively) deferred to the rig.
2026-09-06T13:09:01.9231375Z - Required stages: impl, unit
2026-09-06T13:09:01.9231404Z 
2026-09-06T13:09:01.9231524Z ### REQ-PICKER-PURGE-SHORTCUT
2026-09-06T13:09:01.9235186Z - Title: C-3 (REMOTE-TRUTH triage §C-3 #8): the pick-existing list gains an `x` purge shortcut — on an OFFLINE LOCAL highlight, `x` opens a small in-TUI confirm screen (Screen::ConfirmPurge, the B-2 ChangeAdapterPick shape) and Enter purges via the ONE existing purge core `cmd_endpoint_purge(id, yes=true, force=false)` — NEVER the core's stdin [y/N] (it fights the picker's raw mode; the confirm screen IS the confirm). The shortcut INHERITS both purge gates (offline-only + node-local, cli.rs cmd_endpoint_purge / CONTEXT:189): gated-off presses stay on the list and FLASH WHY (online → offline-only, remote → local-only). force=false is deliberate — the model gate is advisory; the core's own offline check is the authority, and a race to online between gate and purge must REFUSE, never stop-then-purge. After a successful purge the picker STAYS (inline outcome, like Shortcut/ChangeAdapter): the row leaves the in-memory list (remove_endpoint, cursor re-clamped) + flash PURGED:{id}. Hint truth (B-1/F029 discipline): the pick legend renders `x purge` ONLY when purge_key_live() — the same predicate the handler gates on. Red-first: purge outcome reachable ONLY from an offline LOCAL highlight (online/remote → no screen change + why-flash).
2026-09-06T13:09:01.9235378Z - Required stages: impl, unit
2026-09-06T13:09:01.9235406Z 
2026-09-06T13:09:01.9235510Z ### REQ-PICKER-BACK-NAV
2026-09-06T13:09:01.9238105Z - Title: C-4 (REMOTE-TRUTH triage §C-4 #9): Backspace is a back() ALIAS across the picker — one keypress backs out one screen along the SAME reverse map Esc walks (model back(), complete for all screens incl. the C-3 ConfirmPurge), and from the kind layer it cancels the picker (Esc parity) — EXCEPT the two text-edit contexts, where Backspace stays CHAR-DELETE: CreateId entry (id_backspace) and the pick-list filter mode (filter_backspace). DELIBERATE: no empty-buffer fallthrough to back() in the text contexts — mixing delete and nav on one key invites miskeys mid-typing; Esc already backs out (the triage's optional extra, declined). Pure key routing in handle_key (picker/mod.rs) ahead of the per-screen arms; zero model change (the reverse map pre-existed). Red-first: Backspace on Confirm → PickExisting (pre-fix: dead key); on CreateId with a buffer → buffer shortens, screen unchanged; empty buffer → STILL no nav.
2026-09-06T13:09:01.9238349Z - Required stages: impl, unit
2026-09-06T13:09:01.9238382Z 
2026-09-06T13:09:01.9238472Z ### REQ-RC-RECONNECT
2026-09-06T13:09:01.9244160Z - Title: B-3 (REMOTE-TRUTH triage §B-3, the operator-asked UX): the rc attach viewport RECONNECTS on a severed transport instead of print-and-exit. Pre-fix rc was one-shot (resolve→dial→attach→pump→parting line); FAULT-MATRIX row 9 over-promised. FIX: the establish sequence (daemon ensure → broker conn → session resolve local-first/cross-node → dial → attach-open w/ A-4b tracing retry → subscribe) is factored into establish_attach and run_attach_inner wraps establish+pump in a loop. RECONNECTABLE class = severed transport ONLY: PumpEnd::BrokerGone (broker-conn EOF class, broker bounce) + the NEW PumpEnd::Severed (serve-side stream EOF AFTER rendered output = remote conn drop — pre-fix MISLABELED as 'detached — still running'; a nothing-rendered EOF stays the honest NoLiveSession refuse). FINAL ends (Exited/Detached/Displaced/Stalled/NoLiveSession) never re-drive — re-attaching a deliberately-ended session is wrong. On sever: full-screen centered 'Reconnecting to {target}…' banner (pure byte-emit like StatusRow; target = owning-node label or 'local daemon'; Q4 UX rule — operator language, internal sever detail never paints), then re-drive establish_attach every RECONNECT_PAUSE (1s) inside RECONNECT_WINDOW (30s, generous for a daemon bounce); a Detach keypress mid-window aborts honestly to [detached]; window expiry → PumpEnd::ReconnectGaveUp with a plain-language give-up line naming the cause, the window, and the retry action (never op/read-err lingo). Per re-establish: fresh OpMinter (ADR-0034 rc tracing per viewport), fresh initial resize (PTY matches the CURRENT terminal), pump-local render cursor resets so the re-serve ring replay REPAINTS the screen the banner cleared. FAULT-MATRIX row 9 made TRUE (F-3), not edited down. Red-first: serve-EOF-after-render → Severed (vs the pre-fix false Detached); only BrokerGone/Severed classify Reconnect.
2026-09-06T13:09:01.9244298Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9244327Z 
2026-09-06T13:09:01.9244435Z ### REQ-SELF-DETECT-PARENT-PID
2026-09-06T13:09:01.9249085Z - Title: E-1 (REMOTE-TRUTH triage §E-1 #7): self-detect leg (c) — the pid-ancestry fallback — ALSO candidates on `rec.parent_pid` (the harness pid, CONTEXT's 'stable session-binding anchor', stamped at bind), not `rec.pid` alone. ROOT: for an spt-hosted endpoint (broker PTY, headless) `rec.pid` is the ephemeral bind-CLI pid, ALREADY DEAD by send time (the F-026 #11 dead-pid class, field-sighted on hall-bf) — never in any sender's ancestry and alive-gated out — so an spt-hosted sender could NEVER resolve self via leg (c): its messages were from-stamped `cli@NODE` (operator #7) and replies bounced NO_PERCH. FIX: detect_self_by_ancestry pushes a second candidate (id, parent_pid) when `rec.parent_pid` is Some + alive; the pure nearest-first matcher (match_self_by_ancestry) is unchanged. LABEL-ONLY, exactly like the rest of leg (c): from-label/routing default, NEVER authentication — authenticate() untouched, the pid-ancestry-for-auth question stays parked (KH 7.3/7.5 separation holds; a wrong label self-corrects, a wrong grant does not). Env legs (a)/(b) stay first. Red-first int (the triage-specified missing test): rec.pid = dead sibling + rec.parent_pid = genuine live ancestor → self resolves (pre-fix None); ancestry-gate control: live-but-non-ancestor parent_pid must NOT resolve. Rider (same cluster, activated separately once doyle rules the fix shape): F-026 #11 dead-pid itself — rec.pid should hold something that stays true, or liveness readers stop trusting it. Cross-node from-stamp proof (spt-hosted B-side sender arrives at A as `<id>@node`, not `cli@node`) rides the [twohost] rig wave rung.
2026-09-06T13:09:01.9249419Z - Required stages: impl, int
2026-09-06T13:09:01.9249444Z 
2026-09-06T13:09:01.9249547Z ### REQ-HAZARD-SELF-DETECT-TIE
2026-09-06T13:09:01.9253985Z - Title: releases#240 (CONDUIT #236 respin RCA, measured 2026-08-29): self-detection leg (c) must REFUSE an AMBIGUOUS ancestor instead of answering from directory enumeration order. ROOT: match_self_by_ancestry walked the ancestry nearest-first and took the FIRST candidate at the nearest matching ancestor, so when two or more DISTINCT top-level perches recorded the same live pid (rec.pid or the E-1 rec.parent_pid arm, [[REQ-SELF-DETECT-PARENT-PID]]) the answer was whichever id read_dir happened to return first. Measured on one head at one sha: candidates [(bob, P), (alice, P)] on ext4 chose bob, candidates [(alice, P), (bob, P)] on NTFS chose alice; cli.rs then published MSG_OUT with that id as owner and the sink filed it under the WRONG endpoint. The Windows green was enumeration order landing heads, not the code being right, and the order is arbitrary PER DIRECTORY rather than alphabetical or creation-ordered (the same filesystem produced both orders in one session), so no platform is safe. The ambiguous state is PRODUCTION-REACHABLE through shipped verbs with no override flags: two `spt api bind` calls from ONE parent shell both succeed and both record that shell as parent_pid (measured construction test, isolated SPT_HOME). Nested Psyche/Worker perches are NOT a route (they live under owlery/<parent>/nested/ and leg (c) scans top-level only). FIX: a tie among DISTINCT ids at the nearest matching ancestor resolves to None with a loud-once diagnostic naming the tied ids and the pid, never a silent pick. Two ids that are the SAME id (the perch matching on both its rec.pid and its parent_pid) are NOT a tie. LABEL-ONLY like the whole of leg (c): None at the send site falls back to the CALLED --from, which is not merely honest but MORE ACCURATE than the coin. Authentication is untouched.
2026-09-06T13:09:01.9254104Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.9254132Z 
2026-09-06T13:09:01.9254234Z ### REQ-HAZARD-DEAD-REC-PID
2026-09-06T13:09:01.9257727Z - Title: E-1 rider (F-026 #11 dead-pid class, doyle-ruled SCOPED 2026-07-05 — KNOWN-HAZARDS 7.34): a dead `rec.pid` on an spt-hosted perch is EXPECTED, not staleness — the recorded pid is the ephemeral bind-CLI pid, which dies immediately after bind (the broker holds the PTY; no resident harness process at that pid). NO reader may alive-gate on `rec.pid` alone: spt-hosted LIVENESS comes from the daemon-managed status field (KH 2.5 — status present ⇒ authoritative, never a per-pid probe); IDENTITY comes from session/ancestry resolution where `rec.parent_pid` (the harness pid, the stable session-binding anchor) is the ancestry candidate (REQ-SELF-DETECT-PARENT-PID). Re-stamping rec.pid with the harness pid (shape (a)) is OVERRULED: ADR-0021 demoted pid to a bind-time seed hint (re-anchoring truth there reverses the design); every pre-existing record keeps the old CLI pid so readers need the scoped discipline anyway (migration hole); blast radius (every rec.pid consumer + KH 2.5 external-perch probe semantics) buys nothing the reader-side fix doesn't. CLASS rule: any newly sighted rec.pid-alive-gating reader gets the same scoped fix and EXTENDS this requirement's evidence — no new REQ per reader.
2026-09-06T13:09:01.9257941Z - Required stages: doc, int
2026-09-06T13:09:01.9257979Z 
2026-09-06T13:09:01.9258093Z ### REQ-SOFT-END-PRESERVES-LIVE-LISTENER
2026-09-06T13:09:01.9264226Z - Title: F-2 (REMOTE-TRUTH triage §F-2, field-repro'd hall-bf 2026-07-04): a /clear must not sever a SURVIVING poll listener's relay address — post-clear owl-path send hit NO_PERCH while ready was present and the inject path healthy. ROOT (source-certain): the relay registry row (id→addr + owning pid, registered by the LISTENER process itself at PollListener::bind, listener.rs:109) is DELETED by the adapter's soft `api session-end` (reporting.rs:231) fired for the DEPARTING session at /clear; but the poll listener SURVIVES /clear (a session-independent process, still bound on its port), so the deletion destroys a TRUE row. The C-2 boundary re-stamp (REQ-HAZARD-BOUNDARY-READY-STRAND) restores ready + status online but CANNOT re-register — only the listener process knows its socket addr — so every subsequent send lookup misses → NO_PERCH forever (until a listener restart re-binds). FIX: the SOFT arm of cmd_session_end unregisters CONDITIONALLY through the single liveness resolver (liveness::is_registry_entry_alive — the KH 2.5-aware resolver clean_stale_entries routes through): a row whose owner is still ALIVE is PRESERVED (the row is LISTENER-scoped truth, not session-scoped; the listener outliving /clear is the designed shape), a dead/offline row is removed (today's cleanup kept). The ERASE arm stays unconditional (a hard wipe orphans any listener; its row dies with the endpoint). Every legitimate teardown keeps its OWN unregister untouched: PollListener close/close_busy/Drop (listener.rs) and the stop verbs (cli.rs:5574/:10924). Defense-in-depth unchanged: a wrongly-preserved dead row still self-heals at delivery (deliver.rs failed-dial sweep, REQ-HAZARD-REGISTRY-STALE-CLEAN). Red-first: soft session-end with a live registered owner → row survives and lookup still resolves (pre-fix: deleted → NO_PERCH).
2026-09-06T13:09:01.9264454Z - Required stages: impl, unit
2026-09-06T13:09:01.9264479Z 
2026-09-06T13:09:01.9264574Z ### REQ-PUBLIC-ERROR-SURFACES
2026-09-06T13:09:01.9272472Z - Title: F-1 (REMOTE-TRUTH triage §F-1, Q4 UX rule, operator-ruled): CLI stderr a non-developer can hit names the OBSERVABLE SITUATION + the NEXT ACTION — never journal/op/brain/store lingo. The sweep's named offenders: (1) `RC_FAIL:{id}: … brain IPC read deadline elapsed` — the brain transport error surfaced RAW through rc's residual Err arm (rc.rs run_attach_inner); operators read 'brain IPC' where the situation is 'the daemon didn't answer in time'. (2) `WOKE_FAIL:{id}: info.json absent or unreadable — not a hosted perch` (resting.rs apply_event miss) — store-file lingo in the one rest-verb line a stale remote row still surfaces cross-node (the qualified-arm D6 case; the A-3 bare-id local path already routes instead). The miss stays SINGLE-SOURCED from NOT_A_HOSTED_PERCH_MARKER (in-process discriminant, resting.rs — reword is compat-safe per its own doc; the drift-pin unit keeps builder+matcher fused). (3) translation_fault never human-rendered (F-030 post-release seed): a broker-stamped input-translation fault (e.g. 'inject worker panicked') was invisible in `endpoint list`/`whoami` while keystrokes silently degraded — rendered now as a SELF-pin annotation exactly like the psyche_host_error pattern (REQ-HAZARD-LIVEHOST-BOOT-RACE), human line + additive skip-if-none JSON field. (4) host_error never human-rendered (A-2 addendum item 2, the half e7c49e2 deferred to this sweep): the daemon's host-level failure REPORT (REQ-WAKE-RESUME-LEG — e.g. a wake-resume refused because the recorded session adapter is not registered) was written to info.json and surfaced ONLY on `spt endpoint wake`'s accepted-not-bound print, so an operator who ran `spt endpoint list` instead saw an endpoint that simply never came back with no stated reason. Rendered now as the THIRD field of the one render pass beside psyche_host_error + translation_fault (`host: FAILED — the daemon could not host or resume this endpoint (<reason>)`), the stored reason carrying its own next action rather than a second one bolted on, plus additive skip-if-none JSON on the two LOCAL DTOs (SelfPinJson + LocalPerchJson). LOCAL-ONLY by the ruled LocalPerchJson `activity` class (doyle 2026-07-30): host_error is a local perch fact read from info.json, so a gossiped remote row OMITS the key rather than guessing and wire carriage waits for a consumer that actually asks — EndpointRowJson (the WAN projection) is deliberately untouched. Kin to banked patterns: public --help no internal codes; 'Updated' not 'trial'. The A-4b retry terminal + B-3 give-up line + A-3 routing strings shipped F-1-clean already — this REQ sweeps the stragglers and is the home for future sightings (extend, don't multiply).
2026-09-06T13:09:01.9272801Z - Required stages: impl, unit
2026-09-06T13:09:01.9272830Z 
2026-09-06T13:09:01.9272939Z ### REQ-WORKER-SID-SYMMETRIC-AUTH
2026-09-06T13:09:01.9275731Z - Title: W-2 (WORKER-TRUTH triage, operator-ruled 2026-07-06): worker verbs go sid-symmetric with every sibling id-scoped verb — worker-start mints NO token and worker-stop takes NONE (token custody is undue adapter burden, ruling via perri). Registration STORES the sid it authenticated (the parent's sid at start; today cmd_worker_start hardcodes session_id="" — worker.rs:44 — so a sid-authed stop compares against empty and refuses 100%). Stop accepts the parent's CURRENT sid OR the stored registration sid (a /clear between start and stop rotates the parent's sid; either rotation endpoint is honest custody — the REQ-PSYCHE-SID-CUSTODY rotation reasoning). Under the ruling the field adapter's existing emission (worker-stop <id> --session-id <parent sid>) becomes contract-correct as-is. Publish the frozen verb shape to the docs-site with the landing wave (perri blind-builds from published docs).
2026-09-06T13:09:01.9275859Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9275892Z 
2026-09-06T13:09:01.9275988Z ### REQ-WORKER-REAP
2026-09-06T13:09:01.9278671Z - Title: W-3 (WORKER-TRUTH triage): worker records must not persist indefinitely past their useful life — 6 dead-pid workers leaked OFFLINE on flynn (kill-paths where SubagentStop never fires: parent killed, abort, timeout). The stored rec.pid is the ephemeral worker-start hook process (dead by design — the REQ-HAZARD-DEAD-REC-PID class; NEVER an alive-gate signal). Honest reap signals: (a) parent-session lifecycle — reap the parent's soft-stopped + orphaned workers at parent session-end/boundary and on parent-death detection (a worker cannot outlive its parent's live session); (b) a generous TTL floor since `created` as belt-and-braces. Soft-stop preservation semantics (REQ-HAZARD-SOFT-CLEANUP: results drain before reap) stay honored — reap after drain-or-expiry, never mid-flight hard-delete (cascade-wipe guard rationale stands). Sister shape: claude_skill_owl doctor D-21 orphan-worker GC.
2026-09-06T13:09:01.9278794Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9278824Z 
2026-09-06T13:09:01.9278925Z ### REQ-WORKER-LIST-VISIBILITY
2026-09-06T13:09:01.9280828Z - Title: V-1 (WORKER-TRUTH triage, operator rider): worker perches leave the DEFAULT `spt endpoint list` view — they are process-local machinery, not subnet citizens; leaked-or-live worker rows rendering as permanent OFFLINE endpoints is the operator-visible symptom root. A dedicated flag (--workers) reveals them (one command + flag per the --all/--detail precedent, NOT a separate list-working command — sister divergence deliberate). Applies to the human render, --json (additive default-absent filter), and the registry/projection legs; verify-and-stop any worker gossip into the subnet registry as peer endpoints.
2026-09-06T13:09:01.9281090Z - Required stages: impl, int
2026-09-06T13:09:01.9281119Z 
2026-09-06T13:09:01.9281219Z ### REQ-WORKER-PICKER-EXCLUDED
2026-09-06T13:09:01.9282417Z - Title: V-2 (WORKER-TRUTH triage, operator rider): non-drivable endpoint classes never render as picker rows — a worker perch cannot be driven, instantiated, or controlled; offering it is a lie the picker then fails on. Filter endpoint_type worker (and the psyche class if it ever surfaces — same non-drivable family) at every picker source leg, extend-not-multiply for future non-drivable classes.
2026-09-06T13:09:01.9282518Z - Required stages: impl, unit
2026-09-06T13:09:01.9282546Z 
2026-09-06T13:09:01.9282641Z ### REQ-WORKER-MINTED-NAME
2026-09-06T13:09:01.9284229Z - Title: N-1 (WORKER-TRUTH triage, operator rider): worker perch identity is CORE-MINTED and parent-derived — `{parent}-w{N}` with a per-parent counter at registration (sister shape: claude_skill_owl hook_subagent_start.rs) — never the adapter-presented agent id (CC Task ids render as random-named rows). worker-start mints + echoes the id (WORKER_STARTED:{parent}-w{N}); the adapter's agent_id/agent_type ride the record as correlation METADATA, not identity. Verb-shape contract change — freeze with W-2 in ONE coordination with perri.
2026-09-06T13:09:01.9284392Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9284420Z 
2026-09-06T13:09:01.9284537Z ### REQ-ADAPTER-TEMPLATE-KEY-VALIDATION
2026-09-06T13:09:01.9286733Z - Title: P-1 core half (WORKER-TRUTH triage): `adapter add`/`adapter update` validate every declared role template — command, cwd, and [env] inject values — against the substitution-key catalog (spt-runtime BASE_KEYS + role-specific overrides) and REFUSE registration naming the offending key + role (fail-fast family of the [strings] pointer validation). Field driver: flynn's psyche died on the RETIRED {psyche_dir} key (adapter psyche_resume carried the old psyche_init cwd shape) — a permanent template config fault must die loudly at registration, not at the Nth per-event psyche turn via the 3-strike budget. The catalog stays the single source (runtime.rs FILL_KEYS — 'a catalog key must have a real fill'); validation reads it, never a second list.
2026-09-06T13:09:01.9286844Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9286873Z 
2026-09-06T13:09:01.9286977Z ### REQ-PSYCHE-SPAWN-ENV-PARITY
2026-09-06T13:09:01.9290530Z - Title: P-2 (WORKER-TRUTH triage addendum, perri-filed field finding 2026-07-06): the per-event psyche_resume spawn threads the perch record's CAPTURED read_env stamps into the spawn ENVIRONMENT — the F-027 Half-B env-parity contract (BINDING, design-frozen: read_env captured at creation + stamped on the record + threaded IDENTICALLY to every session spawn; the spawn never reads its own process env for a stamped var) extended to the psyche role the design predates. Field driver: flynn (claude-spt:ccs) — the ccs wrapper relocates the account root via CLAUDE_CONFIG_DIR at PARENT launch and the perch record correctly captured it, but the daemon spawns psyche_resume with bare env → default ~/.claude root → headless 'Not logged in' exit-1 → strike loop; psyche + parent land in DIFFERENT account roots (auth AND root-scoped continuity both break). Core stays harness-agnostic (threads whatever [env] direction=read captured — knows nothing of CLAUDE_CONFIG_DIR). Scope note: this is the URGENT psyche leg of F-027 Half B; the full pre_spawn seam + endpoint-session env threading stays design-parked (F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md) unless operator pulls it forward.
2026-09-06T13:09:01.9290754Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9290783Z 
2026-09-06T13:09:01.9290878Z ### REQ-PAIR-NTP-MULTIHOME
2026-09-06T13:09:01.9293482Z - Title: W1/D1 (JOIN-TRUTH): the ceremony NTP query reaches a server on EITHER IP family — `query_unix_secs` (ntp.rs) must iterate every address `to_socket_addrs()` resolves (not just the first) and bind a socket of the matching family per candidate (IPv4 addr → bind 0.0.0.0:0; IPv6 addr → bind [::]:0), first successful answer wins. ROOT (proven 3/3-FAIL via our exact code on enlyzeam): today `UdpSocket::bind(("0.0.0.0",0))` is v4-only and `send_to(&packet, server)` sends ONLY to the FIRST resolved addr — time.google.com resolves 4×AAAA before any A on a v6-first dual-stack box → the primary server is PERMANENTLY unreachable via our code (w32tm reaches it over v6), silently halving NTP redundancy (pool.ntp.org v4 carried everything; a DNS rotation making BOTH v6-first would zero it). Fix keeps the lazy-cache/TTL/fallback contract of REQ-PAIR-8 unchanged — only the socket/resolve leg changes.
2026-09-06T13:09:01.9293664Z - Required stages: impl, unit
2026-09-06T13:09:01.9293688Z 
2026-09-06T13:09:01.9293783Z ### REQ-PAIR-NTP-LOUD-FAIL
2026-09-06T13:09:01.9295902Z - Title: W1/D2 (JOIN-TRUTH): total NTP failure (no server on any family answered) is LOUD, not silent — a node running the ceremony on its raw skewed system clock must be visible. ROOT: current_offset_secs (ntp.rs) does `query_offset_secs().unwrap_or(0)` and eprintln's ONLY on a nonzero success, so an all-servers-unreachable refresh is indistinguishable from 'clock agrees'. Fix: log the TRANSITION into all-servers-failed once per refresh (suggested `NTP_TOTP_UNCORRECTED: all NTP servers unreachable — ceremony clock = raw system clock`) and the recovery transition back to corrected; the OFFSET_TTL already bounds refresh cadence so no per-call spam. Fallback behavior (offset 0 → system clock) is UNCHANGED — this adds observability only.
2026-09-06T13:09:01.9296240Z - Required stages: impl, unit
2026-09-06T13:09:01.9296269Z 
2026-09-06T13:09:01.9296373Z ### REQ-HAZARD-CEREMONY-CLOCK-STEP
2026-09-06T13:09:01.9299252Z - Title: W1/D3 (JOIN-TRUTH, KNOWN-HAZARDS): the cached ceremony offset goes stale-WRONG when the OS clock STEPS under a live daemon — an offset measured against the OLD clock keeps applying for up to the 15-min TTL. Field-proven timeline (enlyzeam): refresh cadence 15:08/15:23/15:38/15:53; operator `w32tm /resync` stepped the clock −210s at 15:45:46; every `subnet join` returned NO_SEED_HOLDER until a daemon bounce forced a fresh query. Fix: the cache snapshot stores an (Instant, SystemTime) PAIR; on read, if |wall-elapsed − mono-elapsed| > ~2s the clock stepped ⇒ force an immediate refresh (offset recomputed against the new clock). PLUS: `meet_seed_holder` (pairhost.rs), on search-deadline exhaustion, forces ONE fresh NTP refresh + one final sweep before returning NO_SEED_HOLDER — so a stepped-clock join self-heals without a bounce. Clock reads must be seam-injectable for the hazard unit (inject the (mono,wall) pair — do NOT sleep 15 min).
2026-09-06T13:09:01.9299363Z - Required stages: impl, unit
2026-09-06T13:09:01.9299387Z 
2026-09-06T13:09:01.9299486Z ### REQ-JOIN-VERBOSE-CLOCK
2026-09-06T13:09:01.9301591Z - Title: W2/D4 (JOIN-TRUTH): the JOINER side is no longer blind to its own ceremony clock — `spt subnet join --verbose` prints the joiner's derived TOTP step, the applied offset seconds, and the NTP correction state (corrected / uncorrected) per meet sweep; the same triple folds into `meet_failure_detail` so the NO_SEED_HOLDER verbose block carries it. ROOT: diagnosing enlyzeam required shipping a compiled probe over ssh because the member logs PAIR_MEET_UP step=N but the joiner surfaces nothing about its OWN step/offset — the exact asymmetry that hid D1-D3. Extends REQ-JOIN-DIAGNOSTICS's --verbose without a new knob. CLI help changes → xtask docs gen, no internal REQ codes in clap /// (docs-token gate).
2026-09-06T13:09:01.9301809Z - Required stages: impl, unit
2026-09-06T13:09:01.9301837Z 
2026-09-06T13:09:01.9301939Z ### REQ-JOIN-DEFERRED-ELEVATION
2026-09-06T13:09:01.9305044Z - Title: W2 (JOIN-TRUTH, operator UX ruling verbatim): 'if --code was not supplied, don't spawn the elevated subnet-join window until a target machine is discovered → just in time for the code prompt.' When --code is ABSENT and the process is UNELEVATED, run the name prompt + ALREADY_MEMBER check + ensure_daemon + the MEET phase (brain.pair_meet) UNELEVATED; only on MetMember spawn the elevated window via the EXISTING try_auto_elevate machinery (the elevated re-run re-executes the join flow — its second meet is cheap, the member is proven present). A FAILED search must NEVER show a UAC/sudo/pkexec prompt. The --code path is UNCHANGED (gate-first, one-shot). The unelevated phase performs ZERO trust mutation — meet is pre-trust per REQ-JOIN-TWO-PHASE/ADR-0030. RULED OUT (doyle): cross-elevation hold-session adoption (passing the daemon-held pair session_id into the elevated process) — a new security seam we don't need; the elevated re-run re-meets instead. The elevation gate MOVES from command-entry to the enrollment boundary; discovery is read-only pre-trust.
2026-09-06T13:09:01.9305216Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.9305244Z 
2026-09-06T13:09:01.9305349Z ### REQ-ECHO-IDLE-AGE-GATE
2026-09-06T13:09:01.9310756Z - Title: THE ECHO GATE SPLITS BY ARM INTO TWO SENTINEL FILES, AND ONLY THE WORK-DRIVEN ONE IS AGE-GATED (releases#113 fork 1, re-ruled 2026-08-30 on a corrected premise). MEASURED FIRST, AND THE MEASUREMENT INVERTED THE TICKET: the W0 report recorded the gate as armed by TRANSITION EDGES ONLY (arm_transition_echo) and concluded that an agent working for hours without a transition edge never echoes. FALSE -- cmd_state's idle arm (api/delivery.rs) writes the same sentinel UNCONDITIONALLY unless --no-gate, and that is published contract (harness-contract api.md: reporting idle also arms the echo gate). `api state idle` IS the turn-end report, so a working agent arms at EVERY turn end and the pulse loop fires within one period. spt-core was never missing the work-driven trigger; it was missing the AGE GATE. THE RULED SHAPE IS TWO FILES, NOT A CARRIER INSIDE ONE: (a) the EDGE sentinel, written by arm_transition_echo, fires UNGATED as today -- detach / attention-shift / suspend are rare and attention-correctness wants timeliness, and gating them would drop exactly the fire that matters at detachment risk; legacy supports the split, its boundary and session-edge fires were not age-gated either. (b) the IDLE sentinel, written by the idle arm, fires only at AGE >= 15 MINUTES, carrying the three legacy mechanics: a fresh sentinel answers SkipSentinelFresh{remaining} and the caller schedules a SHORT NEXT PULSE to wake near the crossing (this file only); age reads BIRTHTIME with an MTIME FALLBACK and the degradation is DECLARED rather than silent (birthtime = oldest un-fired turn end, mtime = most recent); and a metadata failure or clock skew FAILS OPEN -- it FIRES. Two files rather than one carrier is the property: an idle write can never overwrite an armed edge (no last-writer-wins downgrade), each file has one writer class, read-and-clear stays per-file, and the fire-time distinction is STRUCTURAL rather than parsed out of a byte someone has to remember to write. THE ARMING CONTRACT IS UNCHANGED and the docs sentence stays true -- reporting idle still arms; what acquires the age discipline is the FIRE.
2026-09-06T13:09:01.9310879Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.9310908Z 
2026-09-06T13:09:01.9311025Z ### REQ-ECHO-BOUNDARY-INPUT-BEFORE-ROTATION
2026-09-06T13:09:01.9315522Z - Title: A SESSION BOUNDARY FIRES AN ECHO WHOSE INPUT IS CAPTURED BEFORE THE SESSION ID ROTATES (releases#113 fork 3, ruled 2026-08-30 off the measurement it ordered). MEASURED at 343df76e: spt-core caught NO delta at a boundary, on three independent arms. (1) cmd_boundary fires no echo and arms no gate -- it rotates the sid, carries an engine-room empowerment, re-stamps ready, appends the session ledger, publishes one IO boundary event, nudges the project index and resurfaces notifs; the three production fire_echo triggers are pulse, rest-transition and signoff, and a boundary is none of them, nor does the departing session's soft api session-end fire one. (2) Even a fire placed after a boundary could not SEE the departing work: fire_echo feeds history(session_id) resolved through the CURRENT sid, and [history] resolves a single {session_id} file -- ADR-0019 states that property in as many words -- so the input after a rotation is the new, empty session. Session-spanning exists in this codebase but belongs to the DIGEST (ledger + last-K, REQ-TERM-6); the echo does not read it. (3) No last-echo watermark exists anywhere (zero hits across crates/), so the delta since the last echo is not a quantity spt-core holds. THE CONSTRAINT IS INPUT-BEFORE-ROTATION, and the shape delivering it (fire pre-rotation, or snapshot the input then rotate) is the implementer's. NO WATERMARK THIS PHASE, ruled: the whole current-session input stands -- it is a SUPERSET of the delta and the Psyche ingest consumes communes as content-deltas anyway; delta-since-last-echo semantics would be its own request. The boundary echo must not be able to FAIL the boundary: the harness has already rotated by the time core is told, so an echo problem is loud and best-effort, never a refusal that desyncs spt's record from the reality it describes.
2026-09-06T13:09:01.9315823Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.9315851Z 
2026-09-06T13:09:01.9315943Z ### REQ-ECHO-DROP-DIR-RESOLVE
2026-09-06T13:09:01.9318644Z - Title: W1 (LIFECYCLE-TRUTH): fire_echo resolves the manifest commune_dir through the SAME resolver its siblings use before any write. ROOT (pinned): fire_echo (spt-daemon lifecycle.rs:790) passes the RAW manifest commune_dir into run_echo_commune -> echo.rs:115-117 create_dir_all+join; a relative `.claude` under the WMI-launched daemon's System32 cwd = os error 5 deterministic (two live psyches stamped FAILED on it). Siblings already resolve correctly (ingest ~:583, psyche_drop_file :1072 via resolve_endpoint_drop_dir(raw, cwd)). FIX: fire_echo routes through resolve_endpoint_drop_dir; relative-with-no-cwd = SKIP LOUD (stderr), never a raw relative write — kills the latent-worse variant where a writable daemon cwd writes the drop to a WRONG dir silently (echo communes lost, no error). Hardening riders (same touch, no separate REQ): bounded EACCES retry on the drop write; echo claude spawn gets explicit cwd = endpoint cwd (perri ask).
2026-09-06T13:09:01.9318738Z - Required stages: impl, unit
2026-09-06T13:09:01.9318766Z 
2026-09-06T13:09:01.9318873Z ### REQ-ECHO-BRIEF-DIRECT-ROUTE
2026-09-06T13:09:01.9324510Z - Title: THE ECHO-COMMUNE BRIEF ROUTES STRAIGHT FROM THE SUMMARIZER INTO THE TWO-TIER STORE AND NEVER TRANSITS THE AGENT `<id>-commune.md` DROP PATH (operator-ruled 2026-09-06, releases#276). MEASURED BEFORE THE RULING, on doyle own funnel: every `/clear` that hour was followed by an echo frame 25-45s later, landing exactly in the window where an unread Self drop sits, and 2 of 3 Self communes were lost -- the reborn session reported its own wake text as stale. WHY THIS IS A DESIGN GAP AND NOT A RACE TO PATCH: CONTEXT.md:208 names the drop as the AGENT channel and KNOWN-HAZARDS 6.4 names ONE writer on that path plus the daemon as sole deleter; echo.rs read `the mind` as the Psyche and forgot the Self, while docs/CONTEXT-MEMORY.md:25-33 states the Self DOES write that path at a boundary and that the Self signal is the STRONG live-context signal. So the echo -- weak, project-primary, live-conservative -- overwrote the strong Self capture: the exact inversion of the tiering the provenance asymmetry exists to protect. Nothing in the design ever required the brief to transit a file; the file was an implementation convenience that re-used the ingest. THE SHAPE: `run_echo_commune` loses `commune_dir` and returns the provenance-stamped body; the body-to-tiers core of `ingest_drops` is factored into ONE shared `ingest_body` so two callers cannot fork a later tiering change; `fire_echo` calls it, publishes the COMMUNE io-event carrying verbatim what ENTERED the tier (echo header first), consumes the releases#96 intent marker at THAT line rather than at the pulse file sweep, and strikes HARD on a route error through the existing EchoError classification. BEHAVIOUR CHANGE THAT MUST BE STATED IN THE PR: an adapter declaring `[session.echo_commune]` with NO `commune_dir` now echoes, where it used to no-op. Gate: doc -- the harness-contract echo-commune page rewritten to state the direct route, plus the CONTEXT.md:208 and :259 sentences; impl -- echo.rs without the write, ingest.rs `ingest_body`, and the fire_echo route-publish-consume order; unit -- the returned body carries the `Source: echo-commune` header and no drop dir survives in the signature; int -- through a real daemon the COMMUNE io-event appears with that header and NO file appears in `commune_dir`.
2026-09-06T13:09:01.9324963Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9324996Z 
2026-09-06T13:09:01.9325129Z ### REQ-HAZARD-ECHO-NEVER-WRITES-AGENT-DROP
2026-09-06T13:09:01.9327530Z - Title: KNOWN-HAZARDS 6.12 -- THE ECHO BRIEF NEVER TRANSITS THE AGENT DROP PATH. FAILURE (field, 2026-09-06, releases#276): spt-core wrote its echo-commune brief to `<id>-commune.md`, the SAME path an agent writes its own boundary commune to, with no arbitration between the two writers -- so a Self commune authored at a `/clear` and not yet ingested was overwritten UNREAD by the boundary echo firing 25-45s later. Two of three Self communes lost inside one hour on one endpoint. INVARIANT: exactly one writer on the Self drop path -- the agent -- and the echo brief routes direct into the tiers. SPT-CORE MAPPING: `fire_echo` calls `ingest_body`; nothing writes a drop, on any arm. THE TEST IS A BYTE-IDENTITY PIN, NOT AN EXISTENCE CHECK: a pre-placed authored drop carrying Self bytes must be byte-identical after the fire, which goes RED when the write is restored -- an existence assertion would stay GREEN against an overwrite, which is the very failure this hazard names.
2026-09-06T13:09:01.9327682Z - Required stages: impl, unit
2026-09-06T13:09:01.9327710Z 
2026-09-06T13:09:01.9327815Z ### REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS
2026-09-06T13:09:01.9333042Z - Title: W1 (LIFECYCLE-TRUTH): EVERY successful psyche operation clears psyche_host_error — not just the pulse-loop leg. ROOT (three field confirmations, perri): the stamp clears only via note_turn_outcome's Ok leg (lifecycle.rs:1101); a SUCCESSFUL psyche op via checkpoint/wake bypasses it -> stale FAILED stamp sits over a healthy psyche. FIX: event turn, checkpoint/wake synthesis, and signoff echo all clear the stamp on success. ~~AMENDED BAROMETER W1 (2026-08-02, doyle-ruled) -- the UNCONDITIONAL clear this requirement asked for is STRUCK, because it makes a per-kind health stamp impossible: with one current-state field, any turn success erased a standing INGEST fault by construction, so a broken ingest was masked within seconds on a 5s pulse.~~ THE AMENDED FORM: a success clears its OWN kind's hard latch, and -- cross-kind -- the shared timeout latch, since any completed spawn is evidence the load window passed; the stamp is then RECOMPOSED from whatever latches still stand rather than written and cleared imperatively. The original defect stays fixed: a success on ANY leg (event turn, transition/checkpoint echo, signoff echo) still reaches the seam, so a stale stamp over a healthy psyche remains impossible -- what changed is that a success no longer clears a fault it knows nothing about. DECLARED LIMIT, deliberately in the requirement rather than left implicit: the latches live in the host's memory, so a DAEMON RESTART forgets them while the on-disk stamp persists, and after a bounce the first success of any kind clears a stamp whose kind is no longer known. That is the pre-existing behaviour surviving across a restart boundary instead of every few seconds; restart-durable per-kind clearing needs the stored stamp to carry its kind and is filed separately.
2026-09-06T13:09:01.9337813Z - Required stages: impl, unit
2026-09-06T13:09:01.9337970Z 
2026-09-06T13:09:01.9338075Z ### REQ-PSYCHE-ROLE-ABSENT-STATUS
2026-09-06T13:09:01.9344475Z - Title: AN UNDECLARED [session.psyche_resume] ROLE IS A SKIP WITH LOUD STATUS AND ZERO STRIKES, NOT A PER-FIRE TURN FAILURE (releases#229, ruled at the SEMAPHORE grill 2026-08-29; premise re-measured at 9ec181e9 before build). THE ASYMMETRY THIS CLOSES, measured: echo_commune absence takes one warn_once and a skip, while psyche_resume absence reached run_bounded_stdin, which answers RuntimeError::UnknownRole -> TurnError::Runtime -> a FAILED turn -> note_outcome(PsycheKind::Turn) -> a strike, ON EVERY FIRE. THE BUDGET IS FOR A DECLARED ROLE THAT FAILS: a role never declared cannot fail, cannot self-heal, and cannot change between fires, so per-fire strikes exhaust a budget built for a different mechanism and surface as psyche_host_error churn. DETECTION IS STRUCTURAL AND THE SKIP IS DECIDED BEFORE THE TURN RUNS, never inside its failure handling — producing no turn outcome at all is the only shape that spends zero strikes. THE STATUS FIELD IS A NEW SIBLING OF THE PSYCHE FIELD FAMILY, NEVER psyche_host_error (gater-ratified): that field is an error latch carrying a reason, a consecutive-attempts count and the fault slots a stamp was composed from, so routing a configuration fact through it would stamp a non-failure, increment an attempts counter on a structural condition, and churn the field an operator reads to find real faults. THE STAMP MEANS FIRST NOTICED, NOT LAST FIRE: re-recording the same role leaves it untouched, because a moving timestamp would present an unchanged fact as a recurring event. IT CLEARS ITSELF: a fire that sees the role declared clears the status, so a fixed endpoint shows clean with no operator gesture — reachable in practice because the tick reloads a swapped manifest before it fires. THE COMMUNE-SYNC LEG STILL FIRES when the psyche role is absent: they are different roles with independent optionality, and coupling them would let one missing role silence the other's work. BOTH POLARITIES ARE TESTED per the issue's ask — absent means loud once, skipped, zero strikes; declared-but-FAILING still strikes exactly as it does today, which is what keeps the first arm from being a blanket suppression.
2026-09-06T13:09:01.9350291Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.9350447Z 
2026-09-06T13:09:01.9350553Z ### REQ-PSYCHE-ROLE-OPTIONAL-SKIP
2026-09-06T13:09:01.9353215Z - Title: W1 (LIFECYCLE-TRUTH): a manifest with NO [session.echo_commune] role SKIPS commune-sync (debug-level note, no strike) instead of hard-failing the turn. ROOT (perri filing, recovered): missing role -> commune-sync hard-fails -> 3-strike stamps the host ('manifest declares no [session.echo_commune] role') while the published contract presents the role as an optional template. FIX: missing OPTIONAL role = skip, not a turn failure. ROLE OPTIONALITY IS NOT UNIFORM, and the distinction is the point (releases#229): echo_commune is an OPTIONAL TEMPLATE by the published contract, so its absence is unremarkable and its note is quiet; psyche_resume is LOAD-BEARING WHEN DECLARED — an endpoint missing it runs no psyche turns at all — so its absence skips just as loudly-once but must also be STATUS-VISIBLE. See REQ-PSYCHE-ROLE-ABSENT-STATUS, which owns that arm.
2026-09-06T13:09:01.9355540Z - Required stages: impl, unit
2026-09-06T13:09:01.9355694Z 
2026-09-06T13:09:01.9355807Z ### REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE
2026-09-06T13:09:01.9360091Z - Title: W2 (LIFECYCLE-TRUTH, KNOWN-HAZARDS, flagship — the update wedge): PTY viewer fan-out and control mutations must not depend synchronously on a live draining brain. ROOT rig-CONFIRMED (NtSuspendProcess on the brain, no update involved): brain-subscriber session-output writes ride UNDER the per-session log lock (broker.rs:19-20); failed writes are handled (cursor freeze + detach :3486) but BLOCKED writes are not. Suspended brain => within seconds attached rc output freezes; detach does NOT release the control stamp (release routes through the brain); reattach REFUSED (controlled-by); rc --take hangs; daemon status stays healthy. Field: every brain cycle (incl. every update apply) has a freeze window; a stalled/slow-draining new brain = permanent wedge until bounce; brain.ready != subscribers drained. FIX SHAPE (todlando proposes, doyle RULES BEFORE IMPL): subscriber writes move OFF the log lock (bounded/nonblocking, stall => detach-subscriber like viewer eviction — broker already buffers + replays on re-attach, so a detached-stalled brain self-heals by rewind); control stamp release/take completes against the BROKER without brain round-trip (or bounded with loud timeout). doc = KNOWN-HAZARDS entry. Int (tonight's rig, encoded): suspend brain child mid-session -> attached viewer ticks CONTINUE + rc --take completes; resume -> no output lost (cursor replay).
2026-09-06T13:09:01.9363935Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9364103Z 
2026-09-06T13:09:01.9364216Z ### REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL
2026-09-06T13:09:01.9366052Z - Title: W3 (LIFECYCLE-TRUTH): daemon restart no longer massacres hosted endpoints — daemon start RE-RUNS previously-online spt-hosted endpoints. ROOT rig-proven: daemon stop+start (the apply notice's OWN instruction) kills every hosted endpoint; they stay OFFLINE after start (no resurrection) though records exist (info.json status + adapter + cwd). SCOPE RULING (doyle): re-run-on-start, marked start-reason=daemon-restart; agents' minds ride psyche re-host as today. Int: endpoint online -> daemon stop -> start -> endpoint back ONLINE, same id, harness respawned.
2026-09-06T13:09:01.9367713Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9367876Z 
2026-09-06T13:09:01.9367980Z ### REQ-UPDATE-ONE-SHOT-FINISH
2026-09-06T13:09:01.9369804Z - Title: W3 (LIFECYCLE-TRUTH): update apply works daemonless and one command finishes the cycle. ROOT (operator wart): update fetch/apply run ensure_daemon_announced (cli.rs:4386) -> on a stopped box they BOOT THE OLD broker pre-swap, guaranteeing the mixed old-broker/new-brain pair + a manual bounce. FIX: apply works daemonless (swap + record, next start runs new bytes); `update apply --finish` (name subject to docs-token gate) completes the cycle: swap -> brain cycle -> broker restart onto new bytes (rides REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL so the restart is not a massacre). CLI change -> xtask docs gen, no internal codes in clap ///.
2026-09-06T13:09:01.9371587Z - Required stages: impl, unit
2026-09-06T13:09:01.9371736Z 
2026-09-06T13:09:01.9371848Z ### REQ-DAEMON-STOP-LIVE-SESSION-WARN
2026-09-06T13:09:01.9372454Z - Title: W3 (LIFECYCLE-TRUTH, promoted old follow-wave seed): `daemon stop` with live hosted sessions warns + requires --force (or names the sessions it will kill) instead of silently killing them.
2026-09-06T13:09:01.9373069Z - Required stages: impl, unit
2026-09-06T13:09:01.9373216Z 
2026-09-06T13:09:01.9373313Z ### REQ-RC-RECONNECT-TRUTH
2026-09-06T13:09:01.9375129Z - Title: W3 (LIFECYCLE-TRUTH): rc reconnect never auto-starts a daemon and never hangs forever. ROOTS rig-proven (the operator's long-standing 'stop 2-4 times' bug): (a) an rc client's reconnect loop AUTO-LAUNCHES a daemon via WMI (rig: DAEMON_LAUNCH_VIA_WMI from the rc) — resurrection fights the operator's stops; (b) rc freezes at 'Reconnecting to local daemon…' forever when its session died with the broker. FIX: rc NEVER auto-starts a daemon (reconnect only to an already-up broker; loud 'session lost — daemon down' exit otherwise), bounded reconnect with visible countdown.
2026-09-06T13:09:01.9376703Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9376862Z 
2026-09-06T13:09:01.9376965Z ### REQ-DAEMON-STDERR-PERSIST
2026-09-06T13:09:01.9378325Z - Title: W3 (LIFECYCLE-TRUTH, observability): broker + brain stderr tee to a rotating size-capped file under SPT_HOME (e.g. 2x5MB), stamped per generation. ROOT: detached daemon nulls stdio -> the 2026-07-06/07 incident window left ZERO logs (both RCAs ran blind; rigs had to recreate everything). KNOWN-HAZARDS note: never inherit handles (REQ-HAZARD-DETACHED-DAEMON-STDIO) — open the file in-process, don't pipe.
2026-09-06T13:09:01.9379670Z - Required stages: impl, unit
2026-09-06T13:09:01.9379811Z 
2026-09-06T13:09:01.9379915Z ### REQ-UPDATE-PROMOTE-DRAINED
2026-09-06T13:09:01.9383563Z - Title: W3 (LIFECYCLE-TRUTH, mechanic-d MOVED FROM W2 per doyle gate verdict @e5ae7a9 — binding): the update-apply brain-generation promotion completes only when the OLD generation's broker subscriber connection is CLOSED or stall-EVICTED — never while blocked writes still pend on it. ROOT: `brain.ready` != subscribers drained; W2's stall-evict (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) only BOUNDS the false-promote window to BRAIN_WRITE_DEADLINE (15s), it does NOT close it — a new brain can signal ready inside that window while the old gen's conn is still wedged, so the apply 'promotes' onto a still-frozen control plane (the 22:47 incident-night false-promote). FIX: the promotion gate (ADR-0018 brain-trial, brainproc.rs) adds an explicit DRAINED precondition — promote only on ready AND old-gen-subscriber-drained (conn closed OR stall-evicted); the drained signal reads broker truth (the W2 stall-evict tally / the old conn's liveness), no brain round-trip. The residual W2 left open, now closed. Int = a FALSE-PROMOTE rig that exercises the promotion path itself: an old-gen subscriber conn held wedged past ready must NOT promote until it drains (RED-first: ready-alone promotes).
2026-09-06T13:09:01.9386863Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9387016Z 
2026-09-06T13:09:01.9387121Z ### REQ-UPDATE-TRIAL-DRAIN-DRIVE
2026-09-06T13:09:01.9398360Z - Title: UPDATE-WEDGE (counter-54, doyle-ruled 2026-07-09 — regression of the v0.29.0 seamless brain-swap): a brain generation DRIVES the broker's controller-liveness reap (a KIND_SESSIONS poll) each heartbeat throughout its boot/trial loop, so a hard-KILLED prior generation's black-holed LOCAL controller conn (by:None) is stall-evicted within the trial window and can never permanently strand the promotion DRAINED gate. ROOT (2026-07-09 field freeze, `spt update fetch --apply` v0.30.0->v0.30.2 froze all 7 live PTYs ~30s then rolled back): the promote gate (run_trial, brainproc.rs:657-661) needs BOTH `ready_generation==gen` AND `old_gen_drained()`; `old_gen_drained()` = `!any_local_controller_wedged()` (brainproc.rs:534) is a PURE READ of `write_blocked_since` (broker.rs:2703) — it never DRIVES the evict. The evict (`stall_evict_controller`, broker.rs:1039, same 15s `brain_write_deadline` the wedge-read uses) only runs via `reap_dead_controller` (broker.rs:967, severed->drop ELSE stall-evict) inside the KIND_SESSIONS snapshot closure (broker.rs:2879). During the isolated brain-trial window NOTHING polls KIND_SESSIONS: the old brain was hard-killed (`child.kill()`, brainproc.rs:851) so its local controller conn black-holes (live PTYs keep writing to a dead Windows named pipe -> writer BLOCKS, never EOFs) -> `write_blocked_since=Some` -> wedged=true for the full 30s -> gate false -> `WindowElapsedAlive` -> kill+rollback. The candidate DID reach ready (write_ready, brainproc.rs:211, runs before the loop; the v53 log's NET_FAMILY_GATE/PAIR_MEET_UP prove the loop was entered) — so `BRAIN_TRIAL_TIMEOUT: candidate alive but never ready` is the MISLEADING ready-stamped-but-never-DRAINED case, NOT a resume_sessions hang. SECOND LEG (recovery): post-rollback the gen-2 brain's KIND_SESSIONS poll finally reaps -> `BRAIN_SUBSCRIBER_STALL_EVICT:1` fires >15s late -> a session stayed black-holed through recovery -> continued freeze. ONE root, BOTH legs. FIX (BRAIN-SIDE, self-applying — doyle ruled brain-side to AVOID a broker-side coordinated-restart flag): the boot/trial heartbeat loop (brainproc.rs run_brain, currently only `net_status` at :244) also issues `Brain::sessions()` (KIND_SESSIONS, brain.rs:1397 — already exists) every heartbeat, driving the LIVE older broker's ALREADY-SHIPPED reap (>=v0.29.0 LIFECYCLE-TRUTH; the field-stuck broker is v0.30.0 so it HAS it). The old-gen wedged conn is stall-evicted ~15s < the 30s window -> `old_gen_drained()` flips true -> PROMOTE; the same poll on the rollback/recovery brain reaps promptly -> no >15s black-hole -> kills the STALL_EVICT recovery leg. Drive it on the FIRST heartbeat (no one-tick wait) and on BOTH the trial candidate AND the recovery brain (one loop covers both). SELF-APPLIES because it drives the current broker's existing reap verb — v54's brain fixes the v0.30.0->v54 update with NO coordinated broker restart. Forward-compat: brokers <v0.29.0 have neither the DRAINED gate nor the reap, so older-broker updates never hit this path — no regression. Composes with REQ-UPDATE-PROMOTE-DRAINED (the gate this un-strands) + REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE (the stall-evict it drives) + REQ-CONTROLLER-LIVENESS-REAP (the reap verb). Int = the brain-swap-under-live-sessions rig: (1) with a hard-killed prior gen holding a wedged by:None controller past 15s, the new brain PROMOTES within the window AND sessions stay served across the cycle (RED-first: without the drive, WindowElapsedAlive->rollback); (2) a deliberately-failing trial auto-rolls-back AND the restored brain re-drives EVERY session with NO BRAIN_SUBSCRIBER_STALL_EVICT.
2026-09-06T13:09:01.9407908Z - Required stages: doc, impl, int
2026-09-06T13:09:01.9408074Z 
2026-09-06T13:09:01.9408278Z ### REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE
2026-09-06T13:09:01.9414999Z - Title: SEED (DEFERRED, doyle 2026-07-09 — post-counter-54 root-hardening for UPDATE-WEDGE; mint now, impl a FUTURE milestone): on a PLANNED brain-restart (`BRAIN_UPDATE_RESTART`, the seamless update-apply brain-cycle), the outgoing brain's LOCAL (by:None) controller conns are GRACEFULLY CLEAN-CLOSED as the brain is cycled, instead of hard-killed and left to black-hole. ROOT (field-pinned 2026-07-09, daemon.stderr.log L24277-24303): the update-restart path hard-kills the outgoing brain (`child.kill()`, brainproc.rs:851); its live-agent controller conns then block on dead pipes (never EOF) → the broker reads them WEDGED (broker.rs:2695-2700) → the new candidate's promotion DRAINED gate (`any_local_controller_wedged`, broker.rs:2704) stays true until the W2 stall-evict matures (~15s). REQ-UPDATE-TRIAL-DRAIN-DRIVE (counter-54) makes the candidate DRIVE that reap so it promotes within the 30s window — but at a ~15s wedge-maturity hitch (frozen PTYs during the swap). A CLEAN close makes the conn 'simply absent → drained=false AT ONCE → fast promote' (broker.rs:2699-2700), ELIMINATING the hitch = truly seamless (honors the paradigm the field freeze broke). SUPERSEDES the earlier livehost-reattach framing of 'Fix Y': livehost is SPAWN-FRESH (fresh session uuid/pid per boot, nothing to re-attach — wrong site, and it never ran in the trial window); the correct site is the brain-cycle / update-restart path (a bounded graceful-drain of the outgoing brain BEFORE the kill). Non-trivial: hard-kill → bounded graceful drain; a drain that hangs must NOT wedge the swap (timeout then kill anyway, never block the update). Composes with REQ-UPDATE-TRIAL-DRAIN-DRIVE (defense-in-depth reap-drive REMAINS for any conn that still black-holes — a peer/relay conn, a drain-timeout kill) + REQ-UPDATE-PROMOTE-DRAINED (the gate) + REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE (the stall-evict). Int: a planned update-restart under a live-agent controller conn → the new candidate promotes WITHOUT waiting the ~15s wedge-maturity (drained reads false immediately, no STALL_EVICT), RED-first vs the current hard-kill-then-reap-drive ~15s hitch.
2026-09-06T13:09:01.9420843Z - Required stages: 
2026-09-06T13:09:01.9420968Z 
2026-09-06T13:09:01.9421081Z ### REQ-LIVEHOST-RECONCILE-TRIAL-SILENT
2026-09-06T13:09:01.9426903Z - Title: SEED (DEFERRED investigation, doyle 2026-07-09 — UPDATE-WEDGE follow-up): determine WHY the trial/rollback brain's livehost reconcile loop did NOT drive the broker controller-reap (nor re-host the live agents) during the ~30s field update-trial window, when livehost polls `query_live_session_endpoints()` → `brain.sessions()` (KIND_SESSIONS) UNCONDITIONALLY every `LIVE_RECONCILE_INTERVAL_MS`=5000ms (livehost.rs:1026). CONTEXT (surfaced building the counter-54 rig): livehost's 5s KIND_SESSIONS poll drives the SAME broker `reap_dead_controller` sweep the fix drives — so it would otherwise reap the 15s-matured wedge by ~T20 < the 30s trial and SELF-HEAL. It didn't (field froze 30s → rollback), so the field trial-brain livehost was silent/delayed (PIN Q2: no `DAEMON_RESTART_RESUME` under gen-1/gen-2; the 30s kill landed before/around livehost's first reconcile tick). The counter-54 fix (REQ-UPDATE-TRIAL-DRAIN-DRIVE) puts a RELIABLE 500ms reap-driver in run_brain's CORE heartbeat loop, making the wedge-reap INDEPENDENT of livehost — so this does NOT block counter-54. But the livehost silence is a latent anomaly with a SECOND consequence: live-agent HARNESS re-hosting was also delayed ~30s (a separate freeze contributor). Investigate: does `spawn_live_host`'s reconcile thread start promptly on a trial-brain boot, or is its first tick delayed past the trial window? Does its brain conn / `query_live_session_endpoints` block against the swap/wedge state? Register concrete REQ(s) once the mechanism is pinned. RELATED: [[REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE]] (if the outgoing brain's black-holed conns perturb the new brain's livehost conn setup).
2026-09-06T13:09:01.9431646Z - Required stages: 
2026-09-06T13:09:01.9431833Z 
2026-09-06T13:09:01.9431942Z ### REQ-BRAIN-RESUME-NO-CONTROL-STEAL
2026-09-06T13:09:01.9443576Z - Title: UPDATE-WEDGE round 2 (v0.30.4, doyle-ruled 2026-07-09 — field incident on the counter-54 fetch--apply): a brain-respawn must NEVER steal, then stall-evict, the controller of a broker PTY session the daemon brain does not DRIVE. ROOT (field-pinned + SME, docs/UPDATE-WEDGE-2-RCA.md + docs/UPDATE-WEDGE-2-SME-todlando.md): `resume_sessions` (brain.rs:985) re-attaches EVERY session `KIND_SESSIONS` returns via `subscribe` = `subscribe_with(AttachIntent::Control, by:None)` (brain.rs:1450-1455). The docstring's 'a None identity never displaces (falls back to viewer)' is a MYTH for FREE / SAME-LOCAL-IDENTITY slots: `resolve_subscribe` (broker.rs:1134-1153) stall-evicts FIRST, then `become_controller` if the slot is now free OR the incumbent is also local (None==None) — viewer-fallback fires ONLY when a DIFFERENT REMOTE controls. So on a box with N spt-hosted broker PTYs, a brain-respawn STEALS the by:None controller of every free/local-controlled session (incl. the operator's LOCAL `spt rc`), which the daemon brain never drains (it hosts no PTY sessions — brainproc.rs:184) → 15s later `stall_evict_controller` (broker.rs:1039) releases driven_by → the session is UNCONTROLLABLE (Failure A). It also head-of-line-blocks the shared brain↔broker conn on the N-session controller-replay burst → every journaled `spt rc` retake deadlines ('brain IPC read deadline', the REQ-BROKER-ATTACH-JOURNAL-RESILIENT / #16 shared-conn symptom) → global rc failure on ALL N (Failure B). Field 2026-07-09 (operator-confirmed): fetch--apply 0.30.2→0.30.3 PROMOTED CLEANLY (the counter-54 fix worked) but under 7 spt-hosted PTYs (ALL with LOCAL by:None controllers) the resume SILENTLY STOLE all 7 (become_controller same-local re-take, NO Displaced notice → orphaned, output froze immediately, no rc-detach splash) + blocked every rc retake. The 5-vs-2 stall-evict split is ACTIVE-vs-IDLE, not remote-vs-local: 5 producing output → stolen writer blocked >15s → stall-evict; 2 idle → writer parked → no evict, but still silently stolen+frozen. The counter-54 promotion fix did NOT cause this — pre-existing resume-steal latent bug, hidden until N broker PTYs were present at a respawn; the single-black-holed-session A'-rig never exercised N-live-controllers-under-replay. FIX (brain-side): `resume_sessions` re-attaches as **Viewer** (`AttachIntent::Viewer`), NOT Control — a viewer never touches driven_by and is never stall-evicted (broker.rs:1063+ bounded try_send + private eviction), so steal-then-drop vanishes and the operator keeps/regains control; and it relieves shared-conn pressure (a slow viewer is DROPPED, never a 15s controller block) so rc retake gets through. Control ONLY for sessions the daemon brain genuinely DRIVES (empty set today → all become Viewer; forward-correct for the live-agent-adapter future). SECONDARY (escalation, ONLY if the gate shows residual B): stagger the resume re-attach + bound the viewer replay so the respawn burst can't saturate the conn. Int = the multi-broker-PTY-session RESPAWN rig (the coverage the A'-rig lacked): N real broker-spawned sessions with controllers producing output → real brain respawn/promote → assert (1) EVERY session keeps its controller across the swap (no stall-evict of a session the brain doesn't drive), (2) `spt rc` attaches/retakes IMMEDIATELY post-promote (no shared-conn saturation), (3) promotion still succeeds. RED-first: the current Control re-attach steals+evicts + deadlines rc. Composes with REQ-UPDATE-TRIAL-DRAIN-DRIVE (the orthogonal counter-54 promote fix), REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE (the stall-evict it stops mis-firing on non-driven sessions), REQ-BROKER-ATTACH-JOURNAL-RESILIENT (the #16 shared-conn resilience). Distinct from REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE (that = the OLD brain's outgoing black-hole; this = the NEW brain's resume-steal).
2026-09-06T13:09:01.9453728Z - Required stages: doc, impl, int
2026-09-06T13:09:01.9453888Z 
2026-09-06T13:09:01.9454052Z ### REQ-BRAIN-RESUME-NO-CONN-DEADLOCK
2026-09-06T13:09:01.9465141Z - Title: UPDATE-WEDGE round 3 (v0.30.5, doyle-ruled Option A 2026-07-09 — the v0.30.4 field-verify re-wedge, root code-PROVEN + dead-peer-INDEPENDENT): the daemon brain must NOT subscribe broker PTY sessions onto its own request/reply IPC conn — it has no consumer for that output and the subscription DEADLOCKS the conn. ROOT (todlando code-read, docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md; the net-runtime AND the counter-54 reap-drive were both FALSIFIED first — docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md): a conn's send half is a single `SharedSend = Arc<Mutex<SendHalf>>` (broker.rs:78). Subscriber writer threads (`viewer_writer` broker.rs:1333/1342, `controller_writer` :1451) hold `send.lock()` ACROSS a BLOCKING `write_frame`; the dispatch reply path (`send_frame` :4221 → KIND_SESSIONS_REPLY / KIND_NET_STATUS_REPLY) needs the SAME lock. `resume_sessions` (brain.rs:1031→1054) subscribes every session as a Viewer onto the brain's MAIN conn — which is ALSO the brain's request/reply channel. The daemon brain hosts no PTY sessions (brainproc.rs:184) so run_brain never drains that output; it reads the conn only during the 500ms-heartbeat net_status()/sessions() calls (drain-and-DISCARD, `_ => continue`). When an actively-streaming session backs the conn up, a subscriber writer BLOCKS in write_frame holding send.lock() → the dispatch thread can't send the heartbeat reply → net_status()/sessions() never return → the heartbeat loop stalls → the brain never drains → the writer stays blocked = SELF-DEADLOCK on the brain conn's send mutex → every subscriber writer on it wedges → BRAIN_SUBSCRIBER_STALL_EVICT (controller writer blocked >15s). BOTH severities, one mechanism: RESPAWN (resume subscribes N + the replay burst floods the conn before the loop drains) AND STEADY-STATE (an active streamer's output between heartbeats fills the socket buffer). Counter-54 (REQ-UPDATE-TRIAL-DRAIN-DRIVE) added a 2nd per-heartbeat reply round-trip (sessions()) through the contended mutex — WIDENED the window (regression-window-exact), did not create it. The round-2 Viewer fix (REQ-BRAIN-RESUME-NO-CONTROL-STEAL) removed the STEAL but kept the brain a SUBSCRIBER — viewer_writer has the same send.lock()-across-write pattern AND viewers have no stall-evict valve — so v0.30.4 field-verify wedged again. FIX (Option A, brain-side, SEAMLESS): resume_sessions does NOT subscribe (drop the subscribe_with call; §3 verification guard confirmed NO brain consumer — digest/relay/net-consumer/shellwake/presence — reads the resumed subs). The brain conn then carries only request/reply → no subscriber backpressure → no deadlock. Keep the session_cursors seed only if harmless. Rides the brain-swap ⇒ seamless (no daemon.rs:368 broker restart). Option B (a dedicated Split-reader drain of the brain conn, brain.rs:230 — the pump's carrier) is the FORWARD path for when genuinely daemon-DRIVEN sessions land (the live-agent adapter) — deferred, noted, not built. Option C (the broker-side durable CLASS fix) = REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK, deferred. Int = the confirmer rig (brain+broker+PTY, brain_decouple template): N sessions actively producing output, brain resume-subscribed onto its req/reply conn; RED-first = heartbeat stalls + an active-streaming controller stall-evicted >15s WITHOUT any dead peer present (proves dead-peer-independence); assert BOTH severities (respawn interleave + steady-state output backup); Option A turns both green. Composes with REQ-BRAIN-RESUME-NO-CONTROL-STEAL (the round-2 Viewer fix this supersedes as the wedge cure), REQ-UPDATE-TRIAL-DRAIN-DRIVE (the counter-54 reap-drive that widened the window), REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE (the stall-evict it stops triggering).
2026-09-06T13:09:01.9474928Z - Required stages: doc, impl, int
2026-09-06T13:09:01.9475085Z 
2026-09-06T13:09:01.9475191Z ### REQ-DIGEST-GENERATION-SUPERSEDE
2026-09-06T13:09:01.9482873Z - Title: W3 (LIFECYCLE-TRUTH, digest projection truth — flynn filing spt-mobile d0aa3f4): a one-shot `endpoint digest --json` snapshot must return each logical activity row ONCE across a checkpoint/resume, not once per seq-generation. ROOT (spt-core-side, not a consumer bug): the K-session span (digest.rs activity_spanned, SPAN_SESSIONS=5) runs the [digest] extractor per session file and tags each row seq=(ledger_ordinal<<32)|localseq (REQ-DIGEST-CURSOR). A checkpoint/resume (self-/clear + Psyche rebuild) makes the harness REPLAY the prior generation's transcript into the NEW session file, so the ancestor's rows appear in BOTH the ancestor file AND the resume file at the SAME localseq — the span UNIONS them, one logical row surfacing under two full seqs (gen23,local208) + (gen25,local208), identical text/ts/localseq. Consumers dedup by exact seq (the documented authoritative key) so nothing collapses -> duplicate rows in every snapshot / `--after` view (`--follow from:0` is CLEAN — it reads current-generation only; the SPAN is the sole culprit). The trigger cannot disambiguate: `api boundary clear` records SessionTrigger::Clear for BOTH a fresh /clear (disjoint) and a carry-forward checkpoint (reporting.rs:94) — so a structural skip-ancestor needs new boundary metadata + adapter cooperation, deferred. FIX (doyle ruling — flynn Option 1 Supersede, projection-local, source-independent): within the span, collapse cross-generation replay dupes — an Activity record from an OLDER ordinal is dropped when an identical logical record (role, ts, text, tool) exists under a NEWER ordinal; keep the NEWEST-ordinal occurrence so the surviving seq is the live generation (snapshot + follow agree on seq). Cross-generation ONLY (never dedup within one ordinal — a session cannot replay itself; identical within-gen rows are real). Supersede runs on the raw span items BEFORE the window fold (project_timeline) so window_turns counts real turns, not phantoms; and a boundary divider adjacent to a now-fully-superseded ancestor is not left orphaned. Context entries (REQ-TERM-7, single digest.log) are not per-session-spanned -> untouched. Int = a two-session span rig where session B's extracted lines are a superset replay of A (same ts/text at same localseq) + B's own new tail: RED-first (pre-fix shows every A row twice); post-fix each logical row appears ONCE under B's generation, B's tail intact, the /clear boundary marker preserved when A retains rows.
2026-09-06T13:09:01.9489617Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9489775Z 
2026-09-06T13:09:01.9489884Z ### REQ-UPDATE-FINISH-COMMUNE-FLUSH
2026-09-06T13:09:01.9495002Z - Title: DEFERRED (post-LIFECYCLE-TRUTH, operator-ruled 2026-07-07 — mint now, impl a FUTURE milestone): make the update swap LOSSLESS for live hosted endpoints by flushing a final echo-commune per endpoint BEFORE the brain-subtree reap. ROOT (operator-surfaced probing --finish): `update apply --finish` = daemonless swap -> daemon RESTART; the graceful `daemon stop` path (daemon.rs:316-325) raises brain_stop then reaper.reap() KILLS the brain subtree (brain + shellwake watchers + detached Psyches) as one unit — there is NO per-endpoint final commune before the kill. ENDPOINT-SURVIVAL (REQ-UPDATE-FINISH-ENDPOINT-SURVIVAL) then RESPAWNS each orphaned online spt-hosted endpoint, but from its LAST commune (whatever the ongoing per-event echo-commune cadence last saved), NOT an as-of-swap checkpoint — so mid-turn / uncommuned work is lost across the bounce. Today's mitigation is operator discipline: commune-before-swap. FIX (future): the stop/finish path, before reap, drives each LIVE hosted endpoint's final echo-commune (fire_echo final context save) so the respawn resumes from a swap-fresh checkpoint. Composes with ENDPOINT-SURVIVAL (commune -> reap -> respawn) and the W1 echo pipeline (REQ-ECHO-DROP-DIR-RESOLVE / REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS). Bounded + loud per endpoint (a commune that hangs must not wedge the stop — timeout then reap anyway, never block the swap). Int: a live hosted endpoint with uncommuned state -> --finish -> respawned endpoint's digest/psyche reflects the pre-swap state (RED-first: without the flush the respawn shows only the last-cadence commune).
2026-09-06T13:09:01.9499814Z - Required stages: 
2026-09-06T13:09:01.9499938Z 
2026-09-06T13:09:01.9500053Z ### REQ-SELF-ID-TRUST-INJECTED-ENV
2026-09-06T13:09:01.9506192Z - Title: DEFERRED to a followup vX.X.n sprint (post-LIFECYCLE-TRUTH, operator-ruled 2026-07-07): self-identity resolution must trust the harness-injected authoritative id and detect a stomped perch instead of silently mis-attributing. ROOT (doyle /diagnose 2026-07-07, field: agent sends stamped `cli@HFENDULEAM` / mis-attributed): `resolve_from` (cli.rs:5480) stamps `cli@<node>` when `detect_self_id` (roster.rs:103) returns None; detect_self_id resolves self ONLY by reverse-lookup — matching `$OWL_SESSION_ID` against a perch's info.json.session_id (then SPT_AGENT_ID, then parent_pid) — and IGNORES `SPT_ENDPOINT_ID`, the authoritative self-id the adapter injects (present in-env as SPT_ENDPOINT_ID=<id>). When a perch record is STOMPED (a cross-id info.json overwrite — the REQ-SPAWN-COLLISION-GUARD-LIVE-DUP damage class; field case: doyle's live session_id written into the deployah perch), the reverse-lookup mis-resolves (doyle session -> `deployah`) or fails (real deployah -> None -> `cli@node`), and the CLI silently believes the stomped store. FIX: detect_self_id PREFERS `SPT_ENDPOINT_ID` when set+non-empty (the harness-authoritative id, immune to a stompable perch), AND cross-checks it against the reverse-resolved perch id — a mismatch logs LOUD (a stomped/duplicated perch becomes a self-diagnosing signal, not a silent wrong identity). Bare-CLI (no SPT_ENDPOINT_ID) keeps the reverse-lookup then the `cli@node` fallback. Also reconcile the adapter/core self-id env contract (SPT_ENDPOINT_ID vs SPT_AGENT_ID vs OWL_SESSION_ID — which is canonical). NOTE: W4 REQ-SPAWN-COLLISION-GUARD-LIVE-DUP prevents FUTURE stomps but does not heal existing corruption nor add this resolution-robustness; recovery of a live stomp today is a manual `api boundary clear <id> --to-session-id <sid> --session-id <current>` re-bind (doyle recovered the doyle/deployah cross-wire this way 2026-07-07).
2026-09-06T13:09:01.9511389Z - Required stages: 
2026-09-06T13:09:01.9511517Z 
2026-09-06T13:09:01.9511631Z ### REQ-SPAWN-COLLISION-GUARD-LIVE-DUP
2026-09-06T13:09:01.9520244Z - Title: W4 (LIFECYCLE-TRUTH): single-flight wake per endpoint — the WAKE/RESUME respawn seam must not launch twice for one wake. ROOT (perri parentage + recovered filing): one wake processed TWICE within 1s — broker (306368) spawned two identical `launch --cli ccs --id flynn --resume <sid>` 1s apart, both survived; check-then-spawn TOCTOU in the spawn-side guard. DAMAGE: duplicate-perch writers STOMP info.json (the duplicate's compact re-stamped an OLD sid over a fresh /clear rotation -> injects routed to the contended record and lost). FIX: single-flight wake per endpoint (claim on the perch record or broker-side in-flight set keyed by id; second wake within the window = no-op ack), and the spawn path re-checks liveness UNDER the claim. Int: two concurrent wake requests -> exactly one launch tree. #235 AMENDMENT (2026-08-29): the claim's stand-down was bounded by a 2s GLOBAL WAIT whose premise -- a spawn overrunning the ceiling is presumed dead and its claim leaked -- is MEASURED FALSE under battery load: the winner's ConPTY create simply runs past 2s, the loser's deadline expires, and the loser DOUBLE-CREATES (REQ-SPAWN-FRESH-TRUTHFUL falsified once at a W2 battery). Extending the clock only moves the window, because a wait bound is a LIVENESS PROXY. The holder is a THREAD IN THIS BROKER, not a process -- it takes the claim and walks the spawn inline stamping a phase at each step -- so the claim's presence already proves an in-flight spawn (the RAII guard releases on success, on an early error, and on a panic) and an existence probe of the holder would be a tautology. The observable is therefore PROGRESS: the claim carries its holder's last phase and the instant it was stamped, written at the breadcrumb sites that already announce each phase, and the gate stands down for as long as the holder keeps progressing -- however slow it is, with NO clock on a live holder. The ONE condition RAII cannot cover, a holder wedged forever inside one phase, is the only takeover: a claim stalled past its phase's wedge ceiling is taken over LOUDLY, the log line naming the phase and the elapsed that decided it. Ceilings are PER-PHASE and err large on the measured-slow phase (the ConPTY create), because taking over a live holder costs the duplicate session this requirement exists to prevent while waiting on it costs only latency; a ceiling is a wedge detector, never a performance budget. EACH CLAIM CARRIES A GENERATION and a guard releases ONLY ITS OWN: a wedged holder that is taken over may still complete and drop later, and a generation-blind release would free the SUCCESSOR's claim mid-spawn, re-opening this very double-create window one wake later. The availability-over-dup tradeoff of the retired arm survives in its honest form -- a taken-over holder that later completes lands in the AlreadyLive dup surface exactly as before, now reached on evidence of a stall instead of on elapsed time.
2026-09-06T13:09:01.9527795Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9527959Z 
2026-09-06T13:09:01.9528057Z ### REQ-HAZARD-LISTEN-ORPHAN
2026-09-06T13:09:01.9530128Z - Title: W4 (LIFECYCLE-TRUTH, KNOWN-HAZARDS): `api listen --parent-pid N` watches parent liveness and exits loud on parent death. ROOT (mobile-gw RCA): --parent-pid is auth-anchor ONLY — no liveness watch; host death orphans the listener forever -> perch held alive (false ONLINE), EVENTs stream to a dead stdout, dead-owner rebind BLOCKED (recorded pid = the live orphan). FIX: listener watches --parent-pid liveness (Windows: job object or poll; Unix: PDEATHSIG or poll) and exits loud on parent death. flynn's job-object guard (spt-mobile side) stays regardless; filed SPT-CORE-NEEDS §5. Unit: parent-death -> listener exits within one poll window.
2026-09-06T13:09:01.9531957Z - Required stages: impl, unit
2026-09-06T13:09:01.9532100Z 
2026-09-06T13:09:01.9532205Z ### REQ-INJECT-MULTILINE-INTEGRITY
2026-09-06T13:09:01.9534824Z - Title: W5 (LIFECYCLE-TRUTH): the idle-inject TYPED delivery leg delivers multi-line bodies byte-complete. ROOT (4 field instances + spool diff): the typed leg eats HEAD bytes nondeterministically — spool rows complete (1669B) vs ~322B received suffix; mid-turn poll envelopes always intact; a 1854B body later rode the same leg intact => timing race (terminal-readiness / enter-coalescing settle class), NOT a size cap. FIX DIRECTION (todlando proposes on the broker/translate typed-inject seam): settle-before-head, bracketed-paste where the harness supports it, or chunked write with echo-verify. STAKES: live-SENT injects leave NO spool copy — truncation there is unrecoverable. Int: repeated large multi-line injects into a real PTY session arrive byte-complete (loop N times — the race is timing-dependent, single-shot green is not proof).
2026-09-06T13:09:01.9537177Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9537334Z 
2026-09-06T13:09:01.9537435Z ### REQ-HAZARD-INJECT-SETTLE-REARM
2026-09-06T13:09:01.9547170Z - Title: post-0.29.0 (KNOWN-HAZARDS 7.37): the Layer-1 settle-gate must RE-ARM before every delivery on an OBSERVABLE (echoing/interactive) PTY — a mid-session reader reattach re-creates the head-swallow window. ROOT (field-confirmed on 0.29.0, doyle diagnosis + perri screenshot): the shipped W5-A settle-gate (REQ-INJECT-MULTILINE-INTEGRITY) gated Layer 1 behind a worker-local ONE-SHOT (`settled_once`) on the false premise that the head-swallow race is STARTUP-only (reader not attached after spawn). A mid-session `/clear` re-enters the harness's raw-mode input reader, re-creating the pre-settle window — but the one-shot already fired at spawn, so `settle_before_inject` is SKIPPED and the head is eaten again (a checkpoint-wake payload injected right after `/clear` lost its head, mid-path `spt/Cargo.toml)`); echo-verify (Layer 2) is default-OFF for that session, so it is silent + unrecoverable. FIX (doyle ruling): re-settle before EVERY delivery on an observable PTY; latch-skip the steady-state settle ONLY where the probe is UNOBSERVABLE (non-echoing ConPTY — no reader-reattach race to guard, and each settle burns the full deadline). The settle's own bool return (observed vs timed-out) discriminates the class; a re-drive (attempt>1) ALWAYS settles. `settled_once: bool` one-shot → `probe_unobservable: bool` latch driven by the first-attempt settle outcome.
2026-09-06T13:09:01.9551225Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.9551389Z 
2026-09-06T13:09:01.9551499Z ### REQ-IDLE-PARKED-DELIVERY
2026-09-06T13:09:01.9553811Z - Title: W5 (LIFECYCLE-TRUTH): a message QUEUED to an ALREADY-idle spt-hosted endpoint is delivered without an operator poke. ROOT (live during the milestone dispatch 2026-07-07): the idle-edge drain (F-023 leg 2) fires only on the ACTIVE->IDLE transition; no new edge ever comes for a parked session, and the send-time inject didn't carry it — both doyle->todlando dispatches sat delivered=0 in the spool while the endpoint showed ONLINE. FIX: send-time inject fires for an already-idle spt-hosted target (activity sense says idle => inject now, not spool), and/or a bounded spool sweep re-offers pending rows to idle endpoints (piggyback the pulse tick, no new loop). Int: send to a session idle for N minutes -> delivered without any operator poke.
2026-09-06T13:09:01.9555806Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9555964Z 
2026-09-06T13:09:01.9556060Z ### REQ-SPOOL-TAKE-AUDIT
2026-09-06T13:09:01.9557058Z - Title: W5 (LIFECYCLE-TRUTH, RCA cost: proving WHO took delivered=1 rows burned an hour): the spool records the taker per row — leg enum (relay-backlog / hook-poll / idle-inject / psyche) + sid/pid + taken_at ms — surfaced by a --json debug read. Additive column, no schema break (delivered rows already retained).
2026-09-06T13:09:01.9557945Z - Required stages: impl, unit
2026-09-06T13:09:01.9558092Z 
2026-09-06T13:09:01.9558260Z ### REQ-DOC-ECHO-COMMUNE-CONTRACT
2026-09-06T13:09:01.9559848Z - Title: W6 (LIFECYCLE-TRUTH, docs — this gap cost a full outage night, priority slot): publish the [session.echo_commune] I/O contract on the docs-site: key catalog core fills; core does NOT stdin-feed [history] (field-proven); self-locate guidance incl. CLAUDE_CONFIG_DIR / read_env; drop-file protocol (single-writer, ingest-deletes, resolver semantics from W1); stdout ingestion expectations. Public docs use VERSION numbers, never wave codes; docs-publish drift gate applies.
2026-09-06T13:09:01.9561160Z - Required stages: doc
2026-09-06T13:09:01.9561393Z 
2026-09-06T13:09:01.9561494Z ### REQ-DOC-DELIVERY-VOCAB
2026-09-06T13:09:01.9562703Z - Title: W6 (LIFECYCLE-TRUTH, docs — remaining flynn/perri gaps folded): publish the full send-outcome vocabulary (SENT / SENT(WAN) / QUEUED window semantics / DEFERRED / NO_PERCH), digest --json row schema, api poll auth + MAC-stamp prefix, remaining --json shapes checklist (seed #3). Public docs use VERSION numbers, never wave codes; docs-publish drift gate applies.
2026-09-06T13:09:01.9563809Z - Required stages: doc
2026-09-06T13:09:01.9563933Z 
2026-09-06T13:09:01.9564030Z ### REQ-PLATFORM-REGISTRY
2026-09-06T13:09:01.9567374Z - Title: MUSL-TIER W1 (target-triple centralization, behaviour-NEUTRAL refactor): ONE authoritative platform registry from which current_platform(), KNOWN_TARGET_TRIPLES, the applyhost cross-platform 'other' logic, and the asset-name<->triple map all derive. ROOT: the target triple x86_64-unknown-linux-gnu + the implicit 'exactly 2 platforms' assumption are hardcoded across ~6 sites (release.rs current_platform cfg + KNOWN_TARGET_TRIPLES, applyhost.rs:740-743 win/linux binary if/else, xtask asset map, release.yml), so adding any platform (musl, future arm64) is a scattered edit. FIX: a data-driven registry (candidate: SUPPORTED_PLATFORMS const table of {triple, asset_name}) + generalize applyhost 'other' to 'every registered platform except current_platform()'. gnu+windows behaviour BYTE-IDENTICAL — the existing release/update/apply/propagate suites stay green (that is the gate). DESIGN FORK (doyle rules pre-dispatch): enum vs const-table; applyhost N-platform generalization; current_platform stays cfg->triple but output must be a registry member, loud 'unknown' fallback kept.
2026-09-06T13:09:01.9570584Z - Required stages: impl, unit
2026-09-06T13:09:01.9570743Z 
2026-09-06T13:09:01.9570837Z ### REQ-PLATFORM-MUSL
2026-09-06T13:09:01.9573350Z - Title: MUSL-TIER W2 (register the musl platform, self-IDENTIFY): add x86_64-unknown-linux-musl to the W1 registry — a current_platform() cfg arm (target_arch=x86_64, target_os=linux, target_env=musl -> the musl triple, NO more 'unknown' fallback), asset name spt-x86_64-linux-musl, triple-map entry. PROVEN (doyle /diagnose 2026-07-08, backlog #14): the current tree builds+runs static musl with ZERO source changes (rustls not openssl; aws-lc-sys+bundled-sqlite clean under musl-gcc; openpty; DNS/HTTPS works statically) — so W2 is registry DATA + cfg + asset map, NO dependency changes. Gate: a musl-built spt self-reports x86_64-unknown-linux-musl (not 'unknown'); registry-membership unit (the cfg arm is cross-target, unit the registry not the arm); build the musl target in-gate (kitsubito toolchain) and assert current_platform.
2026-09-06T13:09:01.9575568Z - Required stages: impl, unit
2026-09-06T13:09:01.9575715Z 
2026-09-06T13:09:01.9575810Z ### REQ-RELEASE-MUSL-ARTIFACT
2026-09-06T13:09:01.9578474Z - Title: MUSL-TIER W3 (CI build + signed release + update-set publish + self-update E2E): release.yml gains a musl matrix entry (build on kitsubito; install musl-tools+cmake+target in-job, CC_x86_64_unknown_linux_musl=musl-gcc); the assemble job includes spt-x86_64-linux-musl in SHA256SUMS + the release upload; release-publish (xtask) signs the musl artifact; the update-set carries its artifact entry. This closes the field gap: a musl binary today fetches fine but ends UPDATE_FETCH_REJECTED:NoArtifactForPlatform('unknown'). Gate (release-pipeline touch -> real E2E): cut a draft/test release with the musl artifact; a static musl binary on a sub-2.39-glibc box runs spt update fetch -> gets the musl artifact (no NoArtifactForPlatform), verifies SHA256+signature over the musl bytes, applies, self-updates. musl is ADDITIVE — gnu stays the default Linux artifact.
2026-09-06T13:09:01.9580925Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9581088Z 
2026-09-06T13:09:01.9581182Z ### REQ-PUMP-DIAL-FASTFAIL
2026-09-06T13:09:01.9588666Z - Title: PUMP-TRUTH W1 (RE-SCOPED post round-2 empirical lock — the DIAL is EXONERATED, healthy ~100ms): a pump worker-leg PEER-REPLY read to a connect-then-silent / half-alive peer must drop THAT peer as an ORDINARY per-peer failure (peer_outcome's non-TimedOut arm -> PUMP_PEER_FAIL -> drop conn + redial, round CONTINUES, heartbeat advances), NEVER burn the brain's 30s PUMP_PEER_IO_TIMEOUT carrier deadline into a whole-round TimedOut POISON -> supervise_pump doubling-backoff restart. ROOT (deployah leg-instrumented capture, enlyzeam, 3 identical rounds): DIAL_EXIT 96ms ok, LEG i=3 update ms=30025 err[TimedOut] = the wedge. request_update (propagate.rs:373-375) opens the update stream + sends UpdRecord::Query (all bounded, all land), then BLOCKS read_event_until(deadline=call_deadline()=30s) on the peer's Offer/UpToDate reply; a peer that accepts the stream but never answers burns the full 30s -> TimedOut -> peer_outcome (pump/mod.rs:601) POISON -> whole-round abort + restart (= the field PEER_PUMP_FAIL: brain IPC read deadline, always-zero PUMP_PEER_FAIL). request_sync (sync.rs:374-376) is the LATENT TWIN (SKIPS the reply-read only when the want-set is empty; bites the moment it is non-empty against a silent peer). FIX (both legs): (a) reclassify the reply-read no-progress timeout OUT of TimedOut to a non-poison kind (Brain::read_peer_reply_until) so peer_outcome drops ONLY that peer -- poison RESERVED strictly for a genuine broker-IPC-CARRIER desync (the carrier ops net_open_stream/subscribe/send keep raw TimedOut); the abandoned peer stream is safe (exactly-once seq cursor stays contiguous, a late reply matches no live stream id). (b) budget-decouple the reply-read below 30s (Brain::reply_read_deadline = now + min(io_timeout, 10s)) so a silent peer drops promptly even in the still-sequential pre-W2 pump and can never race the carrier deadline. Files: propagate.rs (request_update) + sync.rs (request_sync) + brain.rs (reply_read_deadline + read_peer_reply_until) + pump/mod.rs (peer_outcome poison reserved for carrier-desync). Gate: a connect-then-silent peer at fan#0 -> the update leg drops it ordinarily within the reply-read budget, round continues + heartbeat advances, NO PEER_PUMP_RESTART; happy path (live peer) unchanged; + the sync-non-empty-want-set latent case. Kin REQ-PUMP-PEER-ISOLATION (W2 concurrency, VALIDATED by this root) + REQ-HAZARD-PUMP-IPC-DEADLINE (the poison it must stop mis-firing on a peer).
2026-09-06T13:09:01.9595372Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9595532Z 
2026-09-06T13:09:01.9595633Z ### REQ-PUMP-PEER-ISOLATION
2026-09-06T13:09:01.9599289Z - Title: PUMP-TRUTH W2 (architectural, operator ruling 2026-07-08): one peer must NOT block or poison all others -- peer discovery is async / per-peer-independent. Two coupled defects in run_peer_pump: (1) SEQUENTIAL fan-out (for peer in fan_targets dials one-at-a-time, each up to the bound -> peer N+1 waits behind peer N); (2) WHOLE-ROUND POISON (peer_outcome(...)? -- one TimedOut aborts the ENTIRE round via ? -> supervise_pump doubling-backoff restart, resetting ALL conns). FIX: per-peer concurrency + fault isolation -- the pump issues non-blocking dial requests; the broker (already async tokio+iroh) returns connection/presence results as async events (the D4c presence seam), no serial per-peer block; a peer TimedOut drops + reschedules ONLY that peer, NEVER aborts the round or restarts the pump. Supervised-restart is RESERVED for a dead BROKER conn, not a dead peer (the single-thread+bounded-read A-half REQ-HAZARD-PUMP-IPC-DEADLINE was defensive -- it stopped the infinite wedge but coupled every peer's fate; this decouples). Gate: a mixed roster (1 live + N offline peers) -- the live peer connects AND this node advertises presence in the SAME round the offline peers fail; heartbeat advances every round; no PEER_PUMP_RESTART from a dead peer. Depends on W1 (a fast-failing dial is the precondition for clean per-peer scheduling).
2026-09-06T13:09:01.9603038Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9603195Z 
2026-09-06T13:09:01.9603309Z ### REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE
2026-09-06T13:09:01.9609709Z - Title: F-032 (perri field finding 2026-07-08, LEGACY-SPT-PARITY-GAP, data-loss): commune/signoff ingest MUST NOT delete a drop until its content is DURABLY COMMITTED to every APPLICABLE tier — a slice that cannot be committed this ingest must leave the drop in place for a later ingest (retry when the precondition resolves) OR durably preserve the un-committed slice, NEVER delete-then-lose. ROOT (doyle triage, code-grounded): ingest_drops (spt-live/src/ingest.rs:200) unconditionally `remove_file(&drop_path)?` AFTER route_slices (ingest.rs:121), but route_slices GATES the project tier on `!project_id.is_empty()` (ingest.rs:156) — when the endpoint's cwd is unresolved/owlery-internal at ingest time the project_id is empty, so the `<project-context>` slice is PARSED but never write_context'd/commit_project'd, yet the source drop is still deleted → the project-context content is permanently lost (black-hole). perri's repro: a two-sliced echo-commune (<live-context> role+release recipe + <project-context> v0.17.4 status + Items 3-5 map) INGESTED (file deleted) yet never surfaced at her next SessionStart resume-pull; adapter exonerated (file-write + slicing tags correct); fixture at (system temp)/F-032-commune-2026-07-08T222721Z.md (5595B, sha256 9bc27e18cf385958; perri wrote it verbatim from session log 0841835d.jsonl). Legacy spt held commit-first-then-delete parity; the modern two-slice ingest broke it. FIX distinguishes: a write SUPPRESSED-by-precedence (incoming older than durable → already-superseded → safe to delete) from a slice NOT-committed-because-un-committable-now (empty project_id / write error → must NOT delete; retry or preserve). Gate: an ingest with a non-empty <project-context> slice but an EMPTY project_id must NOT delete the drop (or must durably preserve the project slice) — the content survives to the next resolvable ingest / SessionStart; the live-tier commit path stays unchanged; a genuinely superseded (precedence-suppressed) drop still deletes. KNOWN-HAZARDS entry on landing (REQ-HAZARD-* = conformance-checklist, needs a test).
2026-09-06T13:09:01.9615318Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9615475Z 
2026-09-06T13:09:01.9615581Z ### REQ-LIVE-AGENT-NO-INJECT-DELIVERY
2026-09-06T13:09:01.9623747Z - Title: F-033 RE-SCOPED (doyle re-ruling 2026-07-10 after the #82 gate falsified the original premise — the hosting-mode class split is BINDING context): 'state:live_agent has a self-delivery reader' CONFLATED two hosting modes. (A) HARNESS-hosted live agents (api listen path) DO deliver via adapter channels only — and are ALREADY structurally excluded from inject: bind_from_seed stamps controllable=Some(false), no broker PTY exists. (B) SPT-HOSTED/CONTROLLED live agents' inject leg IS their delivery reader (broker PTY + translation binary — doyle's own endpoint is field proof: ENDPOINT_INJECT + IDLE_PARKED_DRAIN alongside hook-poll); the original blanket state:live_agent exclusion broke REQ-MSG-IDLE-EDGE-DRAIN + the v0.14.3 LAW on both CI platforms and was REVERTED (predicate stays controllable-gated). perri's adapter-channels-only model (F-dupmsg-adapter-confirm.md) holds for class (A) only — do not re-seed the conflation. The F-033 DUPLICATE mechanism (hook-poll + idle-inject both delivering one row, operator spool row 156) is closed structurally by REQ-CARRIER-CLAIM-EXCLUSIVE's atomic cross-carrier take. REMAINING LEGS OF THIS REQ: (a) unit — a harness-hosted live agent (controllable Some(false)/None, no broker PTY) can never route through try_spt_hosted_inject (evidence may TAG the existing is_spt_hosted_no_relay non-controllable case rather than duplicate it); (b) VERIFICATION (report-before-fix, DELIVERED to doyle 2026-07-10): the no-translation-binary raw payload+CR path is PROVABLY DEAD (broker dispatch_endpoint_input: no-binary -> loud spool, never a PTY write — v0.14.3 holds); the operator's typed-unsubmitted garbage is PINNED to the Layer-2 echo-verify RE-DRIVE (broker.rs inject worker) force-enabled host-wide by ambient SPT_INJECT_VERIFY_ECHO in the daemon's inherited dev-shell env (default-OFF declared capability turned on globally — the F-036 env-inheritance class): a false verify-miss RETYPES the whole sequence into the input field. Fix LANDED on the W4 branch (doyle-accepted echo-scrub 2026-07-10): SPT_INJECT_VERIFY_ECHO/SPT_INJECT_FORCE_ECHO_MISS folded into the W1 daemon-startup env scrub (spt_runtime::INJECT_ECHO_ENV_VARS; startup-only, role-spawn builder untouched so explicit per-spawn declaration stays the production on-switch) — evidence rides REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE (the F-036 class REQ). Residual-class RULED (doyle 2026-07-10): this REQ covers the harness-hosted-never-inject predicate leg (unit) + the echo-scrub itself (impl, dual-tagged) — stages [impl,unit].
2026-09-06T13:09:01.9630604Z - Required stages: impl, unit
2026-09-06T13:09:01.9630744Z 
2026-09-06T13:09:01.9630844Z ### REQ-ADAPTER-UNRESOLVED-HINT-FORM
2026-09-06T13:09:01.9633354Z - Title: F-034 leg a (perri/hertz field finding 2026-07-09): the ADAPTER_UNRESOLVED refusal hint must print a WORKING command form. It currently says 'pass --adapter <name[:profile]>', but --adapter is a `spt api` GROUP flag, NOT a `listen` flag — following the hint literally (`spt api listen <id> --adapter <name>`) produces clap `error: unexpected argument '--adapter'` (exit 2). Fix: the hint prints the group-level form, e.g. `spt api --adapter <name> <cmd> …` (a hint the operator can copy-paste and have work). Gate: the ADAPTER_UNRESOLVED message text carries a clap-VALID invocation (group-level --adapter placement) — a unit asserting the hint string parses under the api clap grammar, or at minimum places --adapter before the subcommand. Pure UX/hint-correctness fix, no behavior change.
2026-09-06T13:09:01.9635457Z - Required stages: impl, unit
2026-09-06T13:09:01.9635605Z 
2026-09-06T13:09:01.9635716Z ### REQ-LISTEN-SEED-CONSUME-AFTER-BIND
2026-09-06T13:09:01.9639301Z - Title: F-034 leg b (perri/hertz field finding 2026-07-09, hertz's HEADLINE): `api listen` must NOT consume the consume-once ephemeral seed on a PRE-BIND refusal — validate (adapter resolvable, home/subnet) and BIND first, THEN consume the seed. ROOT: today `api listen` burns the consume-once seed BEFORE it validates home/subnet, so on a multi-subnet node HOME_REFUSED (needs --subnet) fires AFTER the seed is already gone → the corrected retry (adding --subnet) on the SAME pid hits NO_SEED, a dead end (plausibly ADAPTER_UNRESOLVED burns it the same way). A refusal that never bound must leave the seed intact for the corrected retry. Same EFFECT-BEFORE-IRREVERSIBLE-CONSUME ordering class as F-032 (commune commit-before-delete) — the irreversible consume must follow the successful effect, never precede a refusal. Gate: a pre-bind refusal (HOME_REFUSED on a multi-subnet node without --subnet; ADAPTER_UNRESOLVED) leaves the seed CONSUMABLE — the corrected retry on the same pid binds (no NO_SEED); a SUCCESSFUL bind still consumes the seed exactly once (no double-bind). Files: the api-listen bind path (seed consume ordering). Kin F-032 [[spt-core-findings-backlog]].
2026-09-06T13:09:01.9642435Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9642606Z 
2026-09-06T13:09:01.9642712Z ### REQ-LISTEN-SESSION-ID-FALLBACK
2026-09-06T13:09:01.9646473Z - Title: F-034 leg c (perri/hertz field finding 2026-07-09): a session that goes live LATE (hours after SessionStart, or after a daemon restart) must still be able to bind — the ephemeral SessionStart seed ('consumed within seconds') is GONE by then and nothing re-fires it until the NEXT SessionStart, so even `api listen --parent-pid <correct claude pid>` hits NO_SEED. Design assumption 're-fired on the next SessionStart if needed' does not hold for long-lived sessions. FIX (perri-recommended, cleanest): `api listen --session-id <sid>` fallback that binds from the session-id when the pid has no live seed — removes the ephemeral-seed dependency entirely (the adapter already knows the sid; the skill passes it, and can then DROP its manual re-seed step). Alternative (option 1, less clean): re-fire the seed on daemon restart. Gate: a session with NO live seed (expired / post-daemon-restart) binds via `listen --session-id <sid>` (no NO_SEED); the sid-bind carries the same identity/auth the seed-bind would (session_id custody — kin REQ-PSYCHE-SID-CUSTODY / the sid-symmetric-auth pattern). Files: api-listen bind path (sid-fallback seam), clap --session-id flag (plain doc-comment). hertz/perri live-verify; if it lands the adapter skill drops the re-seed step.
2026-09-06T13:09:01.9650081Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9650242Z 
2026-09-06T13:09:01.9650352Z ### REQ-LIST-JSON-LIVENESS-PARITY
2026-09-06T13:09:01.9656615Z - Title: GATEWAY-LIVENESS (flynn field bug 2026-07-09, RCA reader-divergence root): `spt endpoint list` (human) and `endpoint list --json` MUST report an IDENTICAL status for a locally-hosted endpoint — especially a pid-alive, status-ABSENT gateway (no psyche_init). ROOT (todlando RCA STEP-1, doyle-verified): the --json builder (crates/spt/src/cli.rs cmd_endpoint_list) emits each subnet row's status straight from resource_projection (spt-net registry.rs:566, passes instance.status through verbatim :592 — the persisted WAN snapshot, a lagged gossip that can carry a stale/crash-time Suspended) and NEVER applies the self-owned reconcile the human/picker path applies (reconcile_self_owned, crates/spt/src/picker/data.rs:160 via gather_endpoints :112). So a pid-alive self-owned gateway reads Suspended on --json but ONLINE on human (roster::enumerate spt/src/roster.rs:38 -> is_perch_alive pid-fallback spt-store/liveness.rs:136); the adapter suspend-poll (parse_endpoint_status over endpoint list --json --show-all) reads the divergent --json status -> self-suspends a pid-alive gateway. Candidates REFUTED: resource_projection does NOT re-derive liveness (copies instance.status, only skips !routable :582); render is a faithful {:?}. advertised_status (registryhost.rs:822) DOES compute Active via pid-fallback (live advertise fine) — the bug is the READER showing the un-reconciled snapshot. FIX (doyle-ruled): apply the self-owned reconcile in the cli --json builder (SAME spt/picker consumer layer as gather_endpoints, NOT lifted into resource_projection which inverts the dep spt-net->perch), reading LOCAL perch truth (is_perch_alive/unbound -> Active/Offline, mirroring the picker's local_rows). Do NOT force status=online (DEFECT B latent — seed-#5 orphan-listener false-ONLINE risk). Gate: a pid-alive locally-hosted gateway (status-absent, no psyche_init) reads the SAME non-Suspended status on human AND --json. Field-verify flynn (mobile-gw). Kin REQ-PICKER-3 + REQ-PRESENCE-LIVENESS-TRUTH + seed-#5 orphan-listener false-ONLINE.
2026-09-06T13:09:01.9662198Z - Required stages: impl, int
2026-09-06T13:09:01.9662340Z 
2026-09-06T13:09:01.9662448Z ### REQ-HAZARD-BIND-REST-STATE-CARRY
2026-09-06T13:09:01.9665464Z - Title: GATEWAY-LIVENESS DEFECT A (flynn field bug 2026-07-09, confirmed independent): a re-bind MUST preserve the daemon-owned resting intent (rest_state, D9-2/REQ-INST-3) — the same carry-forward discipline establish_perch already applies to cwd/controllable/adapter/read_env. ROOT: establish_perch's record build (crates/spt/src/api/startup.rs, the build closure) constructs a fresh InfoJson via InfoJson::new (defaults rest_state None) and carries cwd/controllable/read_env forward from prior but NOT rest_state -> a re-bind WIPES the wake intent (flynn tick11 rest_state:active vanish). FIX: carry prior.rest_state (and its paired dormant_since_ms anchor, present iff dormant) forward on re-bind, like the sibling fields. Gate: a re-bind over a prior record with rest_state set preserves it (unit — the build closure carries rest_state + dormant_since_ms). KNOWN-HAZARDS entry on landing. Kin REQ-HAZARD-BIND-CWD-UNSET / REQ-PICKER-1 + REQ-INST-3.
2026-09-06T13:09:01.9668057Z - Required stages: impl, unit
2026-09-06T13:09:01.9668199Z 
2026-09-06T13:09:01.9668305Z ### REQ-SEND-WINDOW-DRAIN-HONOR
2026-09-06T13:09:01.9677230Z - Title: F-035 (field finding 2026-07-09): active_only = POLL-ONLY for a relay-bearing live agent -- it must NEVER be RELAY-delivered (its contract is 'active hook window only, never wakes' per spool.rs WINDOW_ACTIVE_ONLY doc + cli.rs:85; `spt send --active-only` / the hidden `--deferred` alias and `send_deferred` shell-context mint it). FIELD SYMPTOM: lia (a full live agent -- relay-for-idle, poll-for-busy) surfaced an --active-only msg on her IDLE RELAY. RCA JOURNEY: v1 RCA (docs/F-035-RCA.md) analyzed the WRONG class (spt-hosted-relay-LESS, the idle-edge inject leg) and proposed a COLLAPSE that would have broken the shipped F-023 anti-starvation gate (docs/F-035-CONFLICT.md); operator reclassified to a relay-bearing live agent; the relay-class re-RCA (docs/F-035-RELAY-RCA.md) traced EVERY active_only->relay carrier and found them ALL ALREADY GUARDED on main@2c05dc9 -- so spt-core has NO code bug. doyle FINAL RULING: the real leak is the ADAPTER's busy->idle poll->idle-representation handoff (spt-claude-code -- a legitimate `api poll` on going idle drains active_only, then the adapter renders it into the idle/relay surface), OUTSIDE spt-core; perri's lane. spt-core DELIVERABLE = a REGRESSION GUARD (tests only, NO behavior change) locking the 3 load-bearing guards that keep active_only off a relay: (1) send_windowed:217 -- an active_only send SKIPS deliver_tcp (never rides a live relay's TCP channel), spools poll-only; (2) cli.rs:5762 -- a cross-node active_only send stays LOCAL-ONLY (the WanMessage wire record has no window field, so shipping it would strip the class and relay-deliver at the far node); (3) relay.rs drain_backlog -> drain_non_deferred (deferred=0) -- the relay backlog NEVER forwards an active_only (deferred=1) row. Guard suite: unit (send_windowed active_only-skips-tcp-to-live-relay + relay_backlog-never-drains-active_only) + int (cross-node active_only stays local-only, never WAN, while a default send to the same remote target DOES take the WAN leg). AMENDED 2026-07-26 (FIELD-TRUTH W1 roll-in, operator ruling): guard (4) added — the idle-edge and parked-re-offer inject claims NEVER take a deferred (active_only) row, on ANY endpoint class. The collapse proposed in docs/F-035-RCA.md v1 and rejected in docs/F-035-CONFLICT.md is now ADOPTED on new field evidence (doyle spool-audit: spt-shells active_only rows taken_leg=idle-inject on a live perch, each starting a turn — the exact 'never wakes' violation this REQ exists to forbid); the F-023 deferred-rescue it collided with is revoked by the REQ-MSG-IDLE-EDGE-DRAIN amendment. Kin REQ-MSG-DELIVERY-AXES + REQ-MSG-IDLE-EDGE-DRAIN + REQ-INST-6.
2026-09-06T13:09:01.9684404Z - Required stages: unit, int
2026-09-06T13:09:01.9684551Z 
2026-09-06T13:09:01.9684699Z ### REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK
2026-09-06T13:09:01.9695293Z - Title: UPDATE-WEDGE round-4 (hertz RCA, root reproduced + source-pinned on Windows 2026-07-09): EVERY write on a physical broker connection rides ONE bounded + cancelable + poison-on-failure framed-write primitive — no writer may hold the connection's serialized send gate across an UNBOUNDED OS write. ROOT: controller_writer held the SharedSend = Arc<Mutex<SendHalf>> guard ACROSS a blocking write_frame; on Windows interprocess 2.4.2 routes the send to WriteFileEx + SleepEx(INFINITE, alertable) with NO supported write timeout (set_timeout → Unsupported), so an `rc --take` controller consumer that stops reading blocks the write INDEFINITELY (~127.95 s in the field capture, released only when a brain restart tore the conns down). Logical stall_evict_controller (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) removes the controller ROLE but neither cancels the in-flight pipe write, closes the physical connection, nor invokes CancelIoEx — the detached writer keeps its SharedSend clone + live stack-owned mutex guard. Load-gated: needs a real seq>0 frame + a non-draining consumer (seq-0 boot conns + a quiescent update are clean — why v0.30.5's controlled apply passed); CTRL_WRITE_LOCKED wait_us=0 on all four capture writers exonerates the mutex convoy — the block is INSIDE the OS write, after lock acquisition. Existing deadlines can't bound it: CONTROLLER_WRITE_DEADLINE is output-driven channel-full handling (a writer blocked on its first owned frame never fills the channel); BRAIN_WRITE_DEADLINE is an age PREDICATE sampled opportunistically, not an I/O timer. FIX (accepted shape, doyle GO + 3 confirms): broker-owned Arc conn object (conn.rs BrokerConn) replacing raw Arc<Mutex<SendHalf>> — serialized write gate (bounded gate-wait) + send half (never leaves the object, so a bypass cannot compile) + idempotent poisoned state + per-in-flight op identity + platform abort seam, with an INDEPENDENT per-conn watchdog firing OUT OF BAND at an ABSOLUTE deadline stamped at write entry covering BOTH gate-wait and OS write completion (brain_write_deadline() — the existing SPT_BRAIN_WRITE_DEADLINE_MS knob; the abort NEVER relies on the write returning or on opportunistic stall-evict sampling). On deadline/partial/cancel/unknown completion: (1) poison the whole physical conn, (2) abort read+write (cfg(windows) CancelIoEx then DisconnectNamedPipe; cfg(unix) UnixStream::shutdown(Both)) so handle_conn reaches existing EOF cleanup, (3) wait for the canceled op to report completion before releasing its buffer (interprocess write_exsync returns only after the completion APC — release = write return), (4) NEVER reuse the conn (a timed-out length-prefixed frame may be partially written), (5) join/finish the retired writer before reporting physical cleanup. Controller (replay + live), viewer, dispatch-reply (send_frame/send_error + inline dispatch_* replies), and nethost stream-log/presence writes ALL route through the primitive — leaving ANY raw unbounded write behind the gate preserves the failure class (grep-proven: no surviving raw send.lock()+write_frame on a physical conn path). NO new output queue (the bounded queue + isolated writer exist; the block is BELOW them); NO PIPE_NOWAIT (recorded mid-frame corruption risk). Broker-side only, no wire change, mixed-version peers wire-compatible; Unix keeps existing semantics under the same poison/retire invariant.
2026-09-06T13:09:01.9704396Z - Required stages: doc, impl, int
2026-09-06T13:09:01.9704558Z 
2026-09-06T13:09:01.9704680Z ### REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE
2026-09-06T13:09:01.9710603Z - Title: MSG-IDENTITY W1 / F-036 leg a (perri field RCA 2026-07-09/10, psyche seat-theft — doyle ACCEPTED primary fix): the daemon MUST sanitize inherited per-session identity env (SPT_ENDPOINT_ID / OWL_SESSION_ID / SPT_AGENT_ID) at startup AND before EVERY role spawn — these are per-session identity and are NEVER correct inherited state for a daemon or its role children. ROOT: a daemon restarted from inside an agent session (routine during core dev / `spt update apply`) carries the session's SPT_ENDPOINT_ID and passes it verbatim to every [session.psyche_resume] spawn; core only strips each role's DECLARED env_remove list (runtime.rs:728), so ONE adapter env_remove miss infects the whole node — every psyche claude turn fires SessionStart, the adapter hook sees the endpoint id, takes the bind path, and ROTATES the victim's perch to the psyche's own sid with a valid prior-sid proof, every pulse (field: lia/deployah/doyle psyches ALL briefed as <sptc-active-perch id=doyle>; 37 peer msgs drained into lia's psyche transcript; victim deliveries eaten, communes dark, sends downgraded from:cli@node). Adapter half FIXED v0.18.8 (env_remove += SPT_ENDPOINT_ID + shim scrub + SPT_PSYCHE_TURN hook-bail) — this REQ is the CORE-LAYER defense so no adapter miss can ever leak identity again. FOLD (F-036 leg b docs-fix, doyle-owned): broaden the recursion_guard_env schema description (manifest.rs:314 + crates/spt-runtime/manifest.schema.json:306) — core honors it on ANY role declaring the field (runtime.rs:740, keyed on the FIELD not the role name); drop the 'summarizer children' wording (perri adopted on both psyche roles v0.18.8, proven live). Gate: a daemon started with SPT_ENDPOINT_ID/OWL_SESSION_ID/SPT_AGENT_ID in its env spawns role children WITHOUT those vars (unit: role-spawn env assembly scrubs the identity set regardless of the role's declared env_remove); KNOWN-HAZARDS entry on landing. Kin psyche-custody/session-pin cluster, [[spt-core-findings-backlog]] F-036.
2026-09-06T13:09:01.9715874Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.9716037Z 
2026-09-06T13:09:01.9716152Z ### REQ-BIND-PSYCHE-CUSTODY-SQUAT-GUARD
2026-09-06T13:09:01.9718875Z - Title: MSG-IDENTITY W1 / F-036 leg c (perri field RCA, doyle ACCEPTED defense-in-depth): a bind whose --set-session-id equals a NESTED psyche perch's own custody sid is definitionally wrong and MUST be refused — core owns psyche-custody.json and can see the collision at bind time. ROOT CONTEXT: with the F-036 env leak, each stolen bind carried a psyche sid as the new pin; the identity-env sanitize (leg a) removes the known vector, this guard makes the CLASS unreachable (any future vector that tries to rotate a real endpoint's perch onto a psyche's custody sid is refused loud). Gate: a bind attempt whose target sid appears in psyche-custody.json as a psyche's OWN sid is REFUSED with a distinct loud token (unit: custody-sid collision refuses; a normal non-custody sid bind is unaffected). Kin REQ-PSYCHE-SID-CUSTODY, session-pin cluster, [[spt-core-findings-backlog]] F-036.
2026-09-06T13:09:01.9721287Z - Required stages: impl, unit
2026-09-06T13:09:01.9721444Z 
2026-09-06T13:09:01.9721544Z ### REQ-CARRIER-CLAIM-EXCLUSIVE
2026-09-06T13:09:01.9726229Z - Title: MSG-IDENTITY W4 / F-033 + operator self-send probe 2026-07-09 (dup-delivery cluster, RCA-FIRST): a spooled message row is delivered by EXACTLY ONE carrier — the first carrier to take a row (hook-poll drain, relay idle-inject, relay-backlog, psyche) atomically CLAIMS it so no other carrier can re-deliver the same row. FIELD EVIDENCE (authoritative, operator-observed): a default-window doyle-to-doyle send while doyle was BUSY delivered on BOTH the busy POLL path AND the idle RELAY path; spool row 156: window='default', delivered=1, taken_leg='idle-inject' — the relay claimed a row a poll also surfaced (REQ-SPOOL-TAKE-AUDIT instrument, already shipped, is the RCA tool: taken_leg/taken_sid/taken_at per row). PRIOR: F-033 (perri 2026-07-08) — the psyche-download filing arrived as TWO copies, dup-delivery live-confirmed. RCA-FIRST (report-before-fix): pin whether the poll drains before/after the relay's delivered=1 mark; whether the busy-to-idle edge re-offers a row a poll already took; whether take-marking is atomic per carrier or check-then-mark racy. DISTINCT from F-035 (that = active_only window honor, spt-core exonerated; THIS = a default msg on both carriers — F-035's lock never asserted a default msg can't ride both). Gate: int — a default send to a BUSY live agent that polls mid-turn AND transitions idle delivers EXACTLY ONCE (spool-audit shows one taken_leg, recipient sees one copy); unit — concurrent take attempts on one row yield one winner. Kin REQ-SPOOL-TAKE-AUDIT, REQ-RELAY-NO-BUSY-DELIVER, REQ-IDLE-PARKED-DELIVERY, [[spt-core-findings-backlog]].
2026-09-06T13:09:01.9730555Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9730709Z 
2026-09-06T13:09:01.9730804Z ### REQ-RELAY-NO-BUSY-DELIVER
2026-09-06T13:09:01.9734252Z - Title: MSG-IDENTITY W4 (operator self-send probe 2026-07-09, companion leg): the relay/idle-inject carrier MUST NOT fire for an ACTIVE endpoint — the daemon already guards send-time (daemon.stderr.log: 'ENDPOINT_INJECT: endpoint ACTIVE -> spool (deferred hint), not injected') yet the field row ended taken_leg='idle-inject' for a message sent while the endpoint was ACTIVE, so the guard is bypassed somewhere on the busy-to-idle EDGE (the parked-drain idle-injects rows that arrived during busy without re-checking whether a poll is concurrently draining them — the edge itself is when BOTH carriers are plausibly live). RCA-FIRST with REQ-CARRIER-CLAIM-EXCLUSIVE (same rig, same instrument); if the exclusive claim alone closes the double-delivery this leg may reduce to an ordering assertion — rule at RCA lock, don't build blind. Gate: unit — the idle-edge drain re-verifies activity (or defers to the claim) before idle-injecting; a row taken by a poll is never idle-injected. Kin REQ-IDLE-PARKED-DELIVERY (the drain this guards), REQ-MSG-IDLE-EDGE-DRAIN, F-023 anti-starvation gate (do NOT break the already-idle delivery class).
2026-09-06T13:09:01.9737695Z - Required stages: unit
2026-09-06T13:09:01.9737829Z 
2026-09-06T13:09:01.9737928Z ### REQ-SEND-STAMP-AGENT-ID
2026-09-06T13:09:01.9742040Z - Title: MSG-IDENTITY W4 / endpoint-identity (operator-flagged 2026-07-09 + live-confirmed on flynn's F-038 ask arriving 'cli@HFENDULEAM'): a live agent's own CLI `spt send <target>` MUST stamp from_id with the AGENT id (e.g. 'doyle'), not the node fallback 'cli@<node>' — today the agent-id stamp rides ONLY the adapter/perch shortform path, so any agent shelling out `spt send` (the DOCUMENTED reach-another-agent form) presents to recipients as an anonymous node CLI: replies mis-route (recipients answer cli@node — no perch — instead of the sender), and the F-036 victim-effect ('sends downgraded to from:cli@node') is indistinguishable from normal CLI traffic. FIX: send-time self-resolve — when the calling process/session maps to a bound live perch on this node (the session-pin/seed machinery already resolves this for bind), stamp that endpoint id as from_id; a genuinely perchless CLI keeps 'cli@<node>'. Gate: unit — a send from a session bound to a live perch stamps the endpoint id; a perchless shell keeps the node stamp; int — recipient's EVENT from= carries the agent id for a shelled-out send from a live session. Kin F-036 victim effects, EVENT envelope (ADR-0020), [[owl-send-not-legacy-spt-send]] (adapter-path stamp works today — this closes the CLI-path gap).
2026-09-06T13:09:01.9745503Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9745665Z 
2026-09-06T13:09:01.9745756Z ### REQ-ENDPOINT-AUTOSTART
2026-09-06T13:09:01.9751267Z - Title: MSG-IDENTITY W5 / F-038 (flynn operator-directed ask 2026-07-10, SPT-CORE-NEEDS #7 + deployah field-confirm same day: mobile-gw alive=false after the v0.30.6 full daemon restart = this feature's absence, live): an endpoint can be marked a STARTUP DEFAULT so the daemon brings it back up at daemon start — Gateway-class endpoints are infra (the phone treats mobile-gw as always-there; box reboot / daemon cold start currently leaves it down until hands-on). SHAPE RULED (doyle, dispatch): (a) `spt endpoint auto-start <id>` persists the endpoint (id + adapter/profile + args) as a startup default REPLAYED at daemon start, symmetric with the shipped `subnet attach/detach --save` precedent — smallest orthogonal cut, explicit operator intent, no interaction with effective_rest_state/F-035 reader-parity semantics (shape (c) restore-what-was-up REJECTED for now: principled but couples to the rest_state neighborhood that just churned; revisit if --save proves insufficient in the field). A saved endpoint that fails to come up logs loud + does not block daemon start or other replays. flynn docs sweep confirmed missing-feature not docs-gap (rest/wake manual-only; no endpoint analog of subnet --save; no manifest field; no api surface). Gate: int — daemon restart brings a --save'd endpoint back up (fresh daemon, saved default, endpoint reaches its steady state without hands-on); doc — public docs page for the verb (VERSION-scoped); unit — persistence round-trip + replay skip-on-missing-adapter loud. Kin subnet --save (the symmetry precedent), REQ-LIST-JSON-LIVENESS-PARITY + REQ-HAZARD-BIND-REST-STATE-CARRY (the F-035 neighborhood shape (c) would have coupled to), [[spt-core-findings-backlog]] F-038. Interim on flynn's box (logon scheduled task) dissolves when this lands.
2026-09-06T13:09:01.9756134Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9756302Z 
2026-09-06T13:09:01.9756407Z ### REQ-DAEMON-SERVICE-INSTALL
2026-09-06T13:09:01.9758644Z - Title: F-038 RIDER (flynn nice-to-have, QUEUED not activated): a documented OS-service registration recipe or `spt daemon install-service` verb so the daemon itself survives box reboot (flynn's box runs managed_by:null = daemon-at-boot unprovisioned; kitsubito's hand-rolled systemd --user unit = prior art). NOT in MSG-IDENTITY scope — REQ-ENDPOINT-AUTOSTART covers the daemon-start-to-endpoint leg; the boot-to-daemon leg stays interim (logon scheduled task / systemd unit). Activate at an infra-provisioning milestone; shape (recipe doc vs verb) ruled then. Kin [[daemon-service-detection-gotcha]] (global-OS-state detection blind on dev box — a verb must not regress that), [[kitsubito-linux-rig]].
2026-09-06T13:09:01.9760757Z - Required stages: 
2026-09-06T13:09:01.9760934Z 
2026-09-06T13:09:01.9761038Z ### REQ-CONN-POISON-DIAL-SCOPE
2026-09-06T13:09:01.9766729Z - Title: MSG-IDENTITY W6 / F-039 (deployah field-acceptance follow-up 2026-07-10, RCA-FIRST — mint per the v0.30.6 PASS handoff): ambient CONN_WRITE_POISONED log-churn correlates 1:1 with PUMP_PEER_FAIL submit-dials to OFFLINE peers (enlyzeam/kitsubito/gravity) with NO wedge and NO freeze — pre-existed the blackhole rig = log-noise/mislabel, not a defect in the r4 fix. CODE CONTEXT: conn.rs poison_and_cancel emits the loud CONN_WRITE_POISONED line for a write that 'exceeded its bound (OR FAILED)' (conn.rs:181) — the fast-FAIL branch (broken pipe / conn refused on an already-dead counterpart) shares the log tag with the TIMEOUT branch that is the field-acceptance wedge observable, so routine conn teardown under offline-peer dial churn reads like poison events. RCA-FIRST: pin the exact write site that fails per PUMP_PEER_FAIL cycle (BrokerConn is broker-side — which broker conn write rides each pump dial failure? status/event fan-out to a departed subscriber? brain-side notification?) BEFORE changing anything — the correlation mechanism is unpinned. FIX SHAPE (post-RCA, doyle rules at lock): reserve the loud CONN_WRITE_POISONED token for the DEADLINE-EXCEEDED class (the wedge observable blackhole-controller.ps1 watches); a plain write-FAIL on an already-dead conn retires quietly (debug-level or a distinct low-noise token). MUST NOT weaken the r4 invariant: every failure path still poisons + retires the conn (REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK behavior unchanged — only the log LOUDNESS is scoped). Gate: unit — deadline-exceeded emits the loud token, fast-fail does not (both still poison); field — offline-peer churn no longer floods daemon.stderr.log with CONN_WRITE_POISONED. Kin REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK (the invariant this must preserve), REQ-PUMP-PEER-ISOLATION (the offline-peer dial neighborhood), [[v0306-published]].
2026-09-06T13:09:01.9771909Z - Required stages: impl, unit
2026-09-06T13:09:01.9772062Z 
2026-09-06T13:09:01.9772157Z ### REQ-CONN-POISON-ATTRIBUTION
2026-09-06T13:09:01.9778100Z - Title: MSG-IDENTITY W6 / F-039 legs b-d (doyle W6 LOCK 2026-07-10, minted per amendment 3): every broker-conn lifecycle record is ATTRIBUTABLE — the W6 RCA's terminal undecidability (per-line 1:1 CONN_WRITE_POISONED churn = fresh-carrier churn OR stderr interleave artifact) exists because records carry no stable conn identity, no role/endpoint/session context, and no timestamps, and the once-per-conn poison latch hides multiplicity. THREE LEGS. (b) IDENTITY: mint a stable per-physical-conn id (monotonic u64 at conn construction — Arc::ptr_eq is the only identity today and it does not survive a log line) plus subscriber role and endpoint/session where known, stamped on CONN_WRITE_POISONED, CONN_WRITE_RETIRED, logical stall-evict, attach/resume/detach, and write-retirement records (RCA attach sites: presence nethost.rs:379, stream nethost.rs:258, controller broker.rs:891, viewer broker.rs:1073). (c) TIME: daemon stderr correlation records carry wall-clock AND monotonic timestamps (stderrlog has neither; broker+brain share one file — interleave is unresolvable without them). (d) LIFECYCLE (doyle-confirmed UNCONDITIONAL, not debug-gated): one BOUNDED set of per-conn lifecycle events — write start/timeout-cancel/transport close/writer exit/replacement-reattach (hertz RCA fix-shape items 1-3). Constraint (doyle LOCK): the split/attribution must not REDUCE total information, only correct its attribution; NO timeout-value changes; NO suppression-as-fix. Gate: unit — lifecycle records carry conn id + role + timestamps; the id is unique per physical conn and stable across that conn's records. Kin REQ-CONN-POISON-DIAL-SCOPE (leg a, the token split these fields ride on), REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS (leg e, consumes these records), REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK (behavior invariant preserved).
2026-09-06T13:09:01.9783679Z - Required stages: impl, unit
2026-09-06T13:09:01.9783980Z 
2026-09-06T13:09:01.9784103Z ### REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS
2026-09-06T13:09:01.9787772Z - Title: MSG-IDENTITY W6 / F-039 leg e (doyle W6 LOCK 2026-07-10, minted per amendment 3 — hertz's five invariants VERBATIM from his RCA fix-shape item 5): 'Build a deterministic black-holed-controller harness against current v0.30.6 semantics and assert: unrelated sessions continue; the bad physical connection is canceled/closed within the bound; its writer exits; a fresh viewer can attach; no lock or task remains owned by the retired connection.' The harness is the standing conformance rig for the r4 SHAREDSEND fix-class — hertz's RCA discipline: only after a timestamped incident maps to a FAILING lifecycle invariant does an ownership/cancellation defect get fixed (the likely shape being complete physical-connection cancellation and writer-task join/retirement, never a broader timeout increase). Consumes REQ-CONN-POISON-ATTRIBUTION's records (conn id + lifecycle events are what make the five assertions checkable deterministically). Kin REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK (the invariant class under test — its brain_decouple int stays the Windows-mandatory gate leg), REQ-CONN-POISON-DIAL-SCOPE.
2026-09-06T13:09:01.9791227Z - Required stages: int
2026-09-06T13:09:01.9791363Z 
2026-09-06T13:09:01.9791472Z ### REQ-UPDATE-GH-TRANSPORT
2026-09-06T13:09:01.9795670Z - Title: THE-FORKENING W1 (ADR-0036, operator-ruled 2026-07-14): the release channel is PRIVATE (`BigscreenVR/spt-bs-releases`) and the gh CLI is the mandated carrier — release discovery (`releases/latest`, cli.rs:9717) and asset download (cli.rs:4861 public browser URLs) move to deadline-wrapped `gh` subprocess calls (`gh api`, `gh release download`; run_git pattern). WHY gh not token+HTTP: private-repo `browser_download_url` 404s even with a valid token — the API asset-id dance is gh's job. Default repo flips via the existing SPT_INSTALL_REPO seam (cli.rs:5363) + xtask REPO const (main.rs:729) + notif.rs consent-changelog URL rider. Loud failure classes: gh missing -> UPDATE_FETCH_REJECTED:GhCliRequired with OS-SPECIFIC install hints (winget/apt/brew); gh unauthed -> distinct GhAuthRequired pointing at `gh auth login`. Signature verification unchanged — bytes verified after download, carrier-independent (update-set/counter/anchor continuity per ADR-0036 §2). release_verify_e2e reworked to the gh carrier. Gate: unit — url/invocation construction + both failure classes render OS-correct hints; int — fetch against a real gh-authed channel resolves latest + downloads and verifies an asset; doc — self-update docs name the gh prerequisite. Kin REQ-INSTALL-BOOTSTRAP-VERB (same carrier at first install), ADR-0036.
2026-09-06T13:09:01.9799664Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9799903Z 
2026-09-06T13:09:01.9800031Z ### REQ-INSTALL-BOOTSTRAP-VERB
2026-09-06T13:09:01.9805092Z - Title: THE-FORKENING W1 (ADR-0036 §3, bootstrap shape b operator-ruled): virgin-box install = `gh release download` the platform binary + ONE self-install verb in the binary itself — the verb places the binary at the canonical install path (the path self-update already respawns from, v0.4.2 lesson), registers user PATH, and leaves first-run identity/daemon-start to the existing idempotent first-run; hosted one-liner install scripts (curl|sh / irm|iex at the dead Pages URL) are RETIRED. Non-interactive (CONTEXT.md Installation: the install path doubles as every adapter's pack-in on-demand install). Windows UAC-740 gotcha binding: the downloaded exe keeps the `spt-*` asset name and the verb lives INSIDE spt — no installer-detection trigger words in exe names. README (bs-core) documents: install gh -> gh auth login -> gh release download -> the verb. Gate: unit — verb places/registers idempotently, refuses cross-platform binaries (platform-stamp check exists, v0.3.2); int — from a clean SPT_HOME+PATH sim, downloaded-binary self-install yields a working `spt` on PATH whose `spt update fetch` then speaks the gh channel; doc — README install section rewritten. Kin REQ-UPDATE-GH-TRANSPORT, REQ-INSTALL-1/2 (the two-paths model this reshapes), ADR-0036.
2026-09-06T13:09:01.9809016Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9809184Z 
2026-09-06T13:09:01.9809293Z ### REQ-DOCS-RELEASE-ASSET
2026-09-06T13:09:01.9812108Z - Title: THE-FORKENING W2 (ADR-0036 §4): every release ships a platform-independent docs bundle `spt-docs.tar.gz` (BUILT mdbook output: HTML + llms.txt + llms-full.txt + raw .md + manifest.schema.json) as a release asset WITH an entry in the SIGNED update-set (sha256, same integrity chain as binaries — docs describe the security-relevant contract surface, they do not ride unverified). Apply lands/refreshes $SPT_HOME/docs (single current copy = docs always match the installed binary). FAILURE ISOLATION binding: a docs-asset failure NEVER fails the binary update — UPDATE_DOCS_SKIPPED loud, retried next fetch. Gate: unit — update-set entry + sha256 verify + skip-loud isolation; int — a fetch+apply lands version-matched docs at $SPT_HOME/docs; doc — self-update docs name the bundle. Kin REQ-DOCS-LOCAL-SERVER (the consumer), REQ-RELEASE-CHANNEL-PRIVATE (the assemble leg), ADR-0036.
2026-09-06T13:09:01.9814608Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9814774Z 
2026-09-06T13:09:01.9814885Z ### REQ-RELEASE-CHANNEL-PRIVATE
2026-09-06T13:09:01.9817229Z - Title: THE-FORKENING W2 (ADR-0036 §2): the publish pipeline targets `BigscreenVR/spt-bs-releases` — release.yml assemble/draft/flip retargeted (draft lands on the bs releases repo, untagged, per the existing spt-releases pattern); docs-publish.yml RETIRED (no public docs, ADR-0014 superseded) with the mdbook build folded into ci.yml as the drift gate (CLAUDE.md mandate: doc generation stays CI-gated); counter + signing key + update-set format CONTINUE unchanged (trust anchor is the continuity, carrier is not). Gate: impl — workflow retarget + drift-gate fold; int — a full release dry-run assembles binaries + docs asset + signed update-set against the private channel. Kin REQ-DOCS-RELEASE-ASSET, REQ-UPDATE-GH-TRANSPORT, ADR-0036.
2026-09-06T13:09:01.9819256Z - Required stages: impl, int
2026-09-06T13:09:01.9819411Z 
2026-09-06T13:09:01.9819520Z ### REQ-DOCS-LOCAL-SERVER
2026-09-06T13:09:01.9823590Z - Title: THE-FORKENING W3 (ADR-0036 §4, operator-ruled crate-over-handroll + port 5474): the daemon (broker side — docs are up whenever the daemon is) serves $SPT_HOME/docs over HTTP on LOOPBACK ONLY, default 127.0.0.1:5474 (+ ::1 where available; 547 rejected — privileged <1024 breaks the Linux user-daemon + IANA dhcpv6-server), config + env override. Implementation = hyper (ALREADY in-tree via iroh/reqwest — zero new supply chain; operator ruled battle-tested crate over hand-rolled HTTP). GET-only; strict path-sanitize under $SPT_HOME/docs (reject .., absolute, encoded traversal); bounded write discipline (conn-hazard class). Published URL surface preserved VERBATIM (llms.txt contract: /llms-full.txt, append-.md raw, /manifest.schema.json — flynn/perri doc habits must not break). Verbs: `spt docs url` prints the resolved URL honoring overrides; bare `spt docs` opens the system browser (cfg(windows) creation_flags no-console). NEVER 0.0.0.0 — a LAN-visible server re-leaks what privating hid. Gate: unit — path-sanitize rejections + content-type map + loopback-only bind config; int — daemon up serves book index + llms-full.txt + a raw .md byte-true from a landed bundle; doc — CONTEXT.md Docs Server entry + docs-site self-reference updated (site-url). Kin REQ-DOCS-RELEASE-ASSET (the producer), ADR-0036, translation-binary console-window gotcha.
2026-09-06T13:09:01.9827351Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9827517Z 
2026-09-06T13:09:01.9827646Z ### REQ-TEST-DAEMON-EPHEMERAL-ADVISORY-PORTS
2026-09-06T13:09:01.9830252Z - Title: TEST DAEMONS ON CO-RESIDENT FLEET RUNNERS MUST BIND EPHEMERAL PORTS FOR ADVISORY SURFACES INSTEAD OF PRODUCTION WELL-KNOWN PORTS. An isolated SPT_HOME isolates disk and broker IPC, not globally-numbered TCP ports; engine-room e2e daemons therefore raced the resident fleet for docs port 5474 and intermittently logged DOCS_SERVER_BIND_FAIL os error 10048 beside unrelated precondition failures. A rig-only explicit environment posture selects port 0 for the daemon docs listener while leaving production config, SPT_DOCS_PORT semantics, and `spt docs url` unchanged. The posture wins over a configured or inherited fixed port, the actual bound ephemeral port remains loud in DOCS_SERVER_UP, and a unit holds a real occupied loopback port while proving the docs listener binds elsewhere. This is the first advisory surface; future advisory listeners reuse the same rig posture rather than minting independent test switches. (IR-62)
2026-09-06T13:09:01.9832829Z - Required stages: impl, unit, int
2026-09-06T13:09:01.9832984Z 
2026-09-06T13:09:01.9833090Z ### REQ-UPDATE-REFUSAL-EXIT-DISTINCT
2026-09-06T13:09:01.9842624Z - Title: A REFUSAL AND A FAILURE ARE DIFFERENT ANSWERS AND MUST NOT SHARE AN EXIT CODE OR A SUMMARY WORD (releases#153, doyle ruling 2026-08-21). A guard that DECLINED and did no work leaves the box exactly as it was; a failure means something broke and may have left work half-done. A caller gating on exit status therefore needs THREE answers: applied (0), refused-with-nothing-done (3), failed (1). CODIFICATION, NOT A NEW CONTRACT — the tree already answered refusals with 3 at three sites (EXIT_NOT_ELEVATED, DAEMON_STOP_REFUSED, UPDATE_FINISH_REFUSED) while cli.rs's own module doc called 1 a 'runtime refusal'; there is no migration to look for, and a fourth code would fork a convention that already reads consistently. WHAT THIS REQUIREMENT COVERS is the places the distinction was genuinely MISSING, which is not where the issue filed it: the adapter sweep's outcome type gains a REFUSED variant carrying the guard's own reason, so a declined release stops rendering FAILED and stops exiting 0 (a sweep that installed nothing must not report success — the filed field shape was a fleet roll recording itself as rolled on the majority of nodes); and the core apply's refused-class arm answers 3 rather than 1. PRECEDENCE: a sweep carrying both a failure and a refusal exits 1 — something broke, and a held rule must not mask it. THE DISCRIMINANT IS DECLINE VERSUS FAULT, NOT WHETHER BYTES MOVED (doyle ruling 2026-08-21, stated here because the tempting wrong rule will be re-litigated by the next reader of ApplyStagedOutcome's doc comments): 3 means policy declined, the system is healthy, change your intent or context and ask again. Rejected (at-rest re-verification — tamper, expiry, key trust) stays 1 even though its own doc says nothing was swapped, because that sentence is a statement about BLAST RADIUS and not about outcome class: what is on disk cannot be trusted, which is a fault. The asymmetry settles it independently of the taxonomy — an &&-gated caller reading 3 carries on, and carrying on is the one reading a tampered artifact must never receive, so filing a security-relevant integrity condition under the benign code would make this contract actively harmful at the site where being loud matters most. Quarantined stays 1 as well: a version rolled back after failing readiness had work happen. The UPDATE_FINISH_REFUSED site is explicitly OUT of scope: it already returns 3 (since 559632e0, shipped in v0.50.0) and is already pinned non-zero from an endpoint context by broker_stop_endpoint_deny_e2e, so the filed face was measured fixed before this lane opened. Gate: doc — the cli.rs exit-code contract sentence and the CONTEXT.md update-composite entry naming what a refusal exits; impl — the REFUSED outcome variant, the sweep's exit aggregation with its precedence, and the refused-class arm; unit — a refusal renders REFUSED with its reason and never FAILED, the sweep exits 3 for refused / 1 for failed / 0 for applied, the mixed sweep exits 1, and the refused apply class answers 3.
2026-09-06T13:09:01.9850709Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.9850871Z 
2026-09-06T13:09:01.9850970Z ### REQ-UPDATE-ADAPTERS-VERB
2026-09-06T13:09:01.9853667Z - Title: THE-FORKENING W4 (operator-grilled 2026-07-14): `spt update adapters [<a>[,<b>...]]` = thin ALIAS over the existing `spt adapter update` engine (cli.rs:748 gh_release avenue; the old verb STAYS — published surface) + comma-list accepted on BOTH forms. Semantics: no names -> all gh_release-avenue registrations; names validated FAIL-FAST against the registry BEFORE any update starts (a typo must not leave a half-updated set); per-adapter failure ISOLATION (one failure doesn't stop the rest) with a per-adapter summary line; nonzero exit if any failed; local-path/dev registrations SKIP loud (not error). Gate: unit — name validation, list parsing, isolation + exit-code aggregation, local-path skip; doc — reference regen (drift-gated). Kin REQ-UPDATE-DEFAULT-COMPOSITE (the caller), REQ-ADAPTER-UPDATE-MESSAGE (per-adapter apply notices ride the summary).
2026-09-06T13:09:01.9856051Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.9856210Z 
2026-09-06T13:09:01.9856314Z ### REQ-UPDATE-DEFAULT-COMPOSITE
2026-09-06T13:09:01.9859603Z - Title: THE-FORKENING W4 (operator-grilled 2026-07-14): plain `spt update` = `update fetch --apply` THEN `update adapters` (core-first doctrine order); when core is already current the core leg no-ops and ONLY adapters update; `--core-only`/`-c` skips the adapters leg. GROUNDING (operator-corrected, code-confirmed): fetch --apply cycles the BRAIN only — broker + PTYs survive (apply_staged applyhost.rs:303; the restart-required text is a NOTICE, cli.rs:4615, not behavior) — so the composite's invoking process survives by construction and NO re-run machinery is needed; on a broker-side release the existing F-025 notice remains the composite's closing output. Gate: unit — composite sequencing incl. already-current -> adapters-only and --core-only skip; int — composite on a staged release applies core then updates a registered adapter in one invocation; doc — reference + self-update docs present plain `spt update` as the primary form. Kin REQ-UPDATE-ADAPTERS-VERB, REQ-UPDATE-RESTART-SAFE-SWAP, REQ-UPDATE-APPLY-RESTART-NOTICE.
2026-09-06T13:09:01.9862489Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9862657Z 
2026-09-06T13:09:01.9862760Z ### REQ-UPDATE-RESTART-SAFE-SWAP
2026-09-06T13:09:01.9866241Z - Title: THE-FORKENING W4 (operator-grilled 2026-07-14; RETIRES findings-backlog seed #12 REQ-UPDATE-ONE-STEP-SAFE-SWAP): `spt update --restart` = the one-step ergonomic path to the SAFE full-cycle swap — fetch -> `update adapters` -> `apply --finish` LAST (lethal-leg-last, ruled: apply --finish restarts the whole daemon incl. broker/PTYs, so it must be the final act — everything completes from ANY invoking context including an spt-hosted session whose PTY dies at that step; accepted cost: a finish FAILURE leaves updated adapters on old-activated core briefly — loud + operator-attended by nature of the flag). Composes with `-c/--core-only` (skip adapters leg). The 0.28.0 wedge lesson closes: the ergonomic one-step no longer picks the riskier path by default for operators who want the full cycle. Gate: unit — flag sequencing incl. lethal-leg-last ordering + -c compose; doc — self-update docs present --restart as the full-cycle form and name the finish-restart consequence. Kin REQ-UPDATE-DEFAULT-COMPOSITE, REQ-UPDATE-FINISH-COMMUNE-FLUSH (deferred commune-flush rides the same finish path when built), seed #12 (retired by this).
2026-09-06T13:09:01.9869529Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.9869687Z 
2026-09-06T13:09:01.9869777Z ### REQ-DAEMON-REFRESH
2026-09-06T13:09:01.9872997Z - Title: THE-FORKENING W4 (operator add 2026-07-14): `spt daemon refresh` — restart the daemon BRAIN without a binary swap and WITHOUT touching the broker: exactly the apply_staged brain-cycle path (brain stop -> respawn -> readiness trial -> promote, incl. the trial-drain drive REQ-UPDATE-TRIAL-DRAIN-DRIVE and viewer-only resume REQ-BRAIN-RESUME-NO-CONTROL-STEAL) minus the swap. Recovery verb for wedged brain-held state (field motivator 2026-07-14: endpoint bringup broken on a live daemon + deployah down — today's only remedy is a full daemon bounce that kills every PTY). Broker + PTYs survive by construction (handoff invariant). Failure = the existing trial rollback semantics (old brain resumes; refresh reports loud). Gate: unit — verb routes the brain-cycle without staging/swap preconditions; int — refresh on a live daemon with a hosted PTY: brain generation changes, PTY survives, endpoint stays attached; doc — daemon docs name refresh next to stop/start. Kin apply_staged (the path it reuses), REQ-UPDATE-TRIAL-DRAIN-DRIVE, REQ-BRAIN-RESUME-NO-CONTROL-STEAL.
2026-09-06T13:09:01.9876734Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9876987Z 
2026-09-06T13:09:01.9877135Z ### REQ-WHOAMI-IDENTITY-ONLY
2026-09-06T13:09:01.9882556Z - Title: PROJECT-INDEX W1 (F-040, perri filing claude-spt docs/SPT-CORE-FINDINGS.md @d775b38; correctness-critical opener — the 2026-07-15 message-bodies incident root): a core IDENTITY-ONLY resolution — session -> endpoint|null — that touches NO list/registry/project/git/network path, and `spt whoami` DE-ALIASED from cmd_endpoint_list (cli.rs ~6609 aliases the full list = 100+ git children under hook deadlines). endpoint-info is DISQUALIFIED as the carrier (runs latest_project_ref). Adapters/hooks get a bounded-time identity verb; the harness-hosted adapter fallback stays deadline-vulnerable until this ships. Gate: impl — the resolver + whoami de-alias; unit — resolver returns endpoint|null with zero project derivation (assert no git spawn seam); int — whoami on a multi-perch home answers fast-path without touching context branches; doc — harness-contract api.md names the identity verb + its no-derivation bound. Kin REQ-PROJECT-INDEX-READER-CUTOVER (list-shaped verbs), REQ-WHOAMI-1, docs/PROJECT-INDEX-TRIAGE.md.
2026-09-06T13:09:01.9887732Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9888033Z 
2026-09-06T13:09:01.9888224Z ### REQ-PROJECT-INDEX-STORE
2026-09-06T13:09:01.9892449Z - Title: PROJECT-INDEX W1 (ADR-0037, RCA .claude/reports/2026-07-10-hertz-session/03): spt-store owns the VERSIONED materialized project-index format + read path. Reader contract: read one compact versioned index, join with the local perch roster, return immediately; stale/missing renders last-known-good or '-'; NEVER fall back to synchronous git enrichment; daemon-offline readers consume the last persisted snapshot; truncated/schema-mismatched index degrades to fast reads + last-known-good, never an error stall. Gate: impl — format + store read path; unit — version/schema-mismatch/truncation degradation legs + join semantics; doc — CONTEXT.md project-index entry + STORAGE.md section. Kin REQ-PROJECT-INDEX-WRITER (the producer), ADR-0037.
2026-09-06T13:09:01.9894920Z - Required stages: doc, impl, unit
2026-09-06T13:09:01.9895106Z 
2026-09-06T13:09:01.9895221Z ### REQ-PROJECT-INDEX-WRITER
2026-09-06T13:09:01.9898927Z - Title: PROJECT-INDEX W2 (ADR-0037): the daemon is the SOLE single-flight project-index writer: load persisted index at startup; ready WITHOUT warm (cold start = daemon ready + CLI fast before background completes); background reconcile with BATCHED complexity O(P+B+F+C) — enumerate branches ONCE, <=1 tree scan per changed branch, ONE derivation per distinct normalized cwd (in-process BranchStore traversal or fixed plumbing calls; backgrounding the existing 100+ process loop is REJECTED); atomic replace; last-known-good preserved on any failure; warm start with unchanged generation performs NO scan. Observability surface: generated time, source generation, pending refresh, last duration/error, endpoint/project/cwd counts, cache hits/misses, stale reads, repair count — index presence alone is not health. Gate: impl — writer + observability; unit — single-flight, atomic-replace, last-known-good, no-scan-on-unchanged-generation; int — cold+warm start legs against a real store; COMPLEXITY COUNTERS are the CI gate (wall-clock = manual acceptance ONLY, shared-runner flake class); doc — daemon docs writer-duty section. Kin REQ-PROJECT-INDEX-STORE, REQ-PROJECT-INDEX-INVALIDATION.
2026-09-06T13:09:01.9902808Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9902980Z 
2026-09-06T13:09:01.9903099Z ### REQ-PROJECT-INDEX-INVALIDATION
2026-09-06T13:09:01.9906052Z - Title: PROJECT-INDEX W2 (ADR-0037): event-driven index invalidation, debounced + coalesced: new session/cwd -> refresh that endpoint + the shared cwd cache; context-store mutation -> ONE debounced global membership refresh keyed on BRANCH-TIP FINGERPRINTS (no authoritative context-commit path exists — a writer-maintained generation counter is REJECTED because nothing maintains it); bind/start, rename, fork, purge -> affected endpoint rows; low-frequency periodic reconcile as the backstop; cwd identity refresh ONLY on path/repo-identity/.git-config change (ordinary commits do NOT alter project identity); multiple invalidations coalesce into one refresh. Gate: impl — the event set + debounce/coalesce; unit — each event class maps to its refresh scope + coalescing proof + ordinary-commit no-op; int — session/context/rename/fork/purge invalidation against a live daemon; doc — rides the ADR + triage doc. Kin REQ-PROJECT-INDEX-WRITER.
2026-09-06T13:09:01.9908566Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9908729Z 
2026-09-06T13:09:01.9908838Z ### REQ-PROJECT-INDEX-READER-CUTOVER
2026-09-06T13:09:01.9912238Z - Title: PROJECT-INDEX W3 (ADR-0037): endpoint list, the picker, and endpoint-info consume the materialized index — NO git work in any user-facing read path (the O(PxB+C) fanout at cli.rs ~2954 / picker/data.rs ~456-518 dies). BEHAVIORAL PARITY is binding: precedence session-cwd -> origin-cwd -> context-recency and rendered project IDs/display names unchanged (parity suite vs the old derivation on a fixture); bare/partial run shares the indexed projection; fully-qualified --adapter+--id direct run stays picker-free; the direct-run 25s broker-session gate stays separately tested/observable. Degradation legs (git unavailable, branch malformed/locked, cwd deleted) keep fast reads. Manual latency acceptance on the 13-perch/7-branch fixture (~30s -> sub-second) + hertz field-verify on HFENDULEAM — NOT a CI wall-clock gate. Gate: impl — reader cutover; unit — parity + degradation; int — list/picker against a daemon-maintained index incl. counters proving zero reader git spawns; doc — reference regen + CONTEXT avoid-list. Kin REQ-WHOAMI-IDENTITY-ONLY, REQ-PROJECT-INDEX-STORE/WRITER/INVALIDATION.
2026-09-06T13:09:01.9915300Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9915467Z 
2026-09-06T13:09:01.9915575Z ### REQ-HEAVY-UNIT-CLASSIFICATION
2026-09-06T13:09:01.9919905Z - Title: A unit test that stands up a REAL broker inside a lib/bin `#[cfg(test)]` block must sit in the `heavy-broker-pty` nextest group, and the classification must be ENFORCED rather than remembered. FLAKE-LEDGER: `attach_viewport_reconnects_across_a_broker_bounce` diagnosed this class in the `spt` binary, wrote the CLASS in prose, then shipped an ENUMERATION of four `rc::tests::` names — so the identical shape in `spt-daemon`'s lib (`applyhost`: a real `Broker::bind` + `serve()` in 10 of its 13 units) stayed in the full-parallel Phase-A pool and TIMED OUT at 240s twice, at v0.32.0 and again under the v0.39.0 W5 gate, the v0.32.0 remedy never having landed. The defect is provable from `.config/nextest.toml` ALONE (two overrides, neither matching `kind(lib)`); timing evidence only ever estimated the rate. Gate: impl — heavy-group overrides for `applyhost`/`livehost`/`pump` (spt-daemon lib) and `wansend` (spt bin, found BY the check rather than by a person), plus `xtask check`'s `check_heavy_unit_classification` keyed on the SHAPE (a `Broker::bind` after the `mod tests` marker) instead of a name list; unit — the two pure seams, including the regression for this check's OWN first draft, which substring-matched `<module>::tests` and so missed every module written inside an alternation group. Kin FLAKE-LEDGER: `attach_viewport_reconnects_across_a_broker_bounce` + the HEAVY-AT-BIRTH ruling (.config/nextest.toml), REQ-CI-DOCS-ONLY-THIN.
2026-09-06T13:09:01.9923914Z - Required stages: impl, unit
2026-09-06T13:09:01.9924063Z 
2026-09-06T13:09:01.9924158Z ### REQ-CI-DOCS-ONLY-THIN
2026-09-06T13:09:01.9927908Z - Title: CI (operator-ruled 2026-07-16, from PR #8 review; registry-only extension operator-approved 2026-07-27; superseded for main pushes by ADR-0050 on 2026-07-29): a PR whose ENTIRE diff is Markdown OUTSIDE docs-site/ and/or the declarative `traceable-reqs.toml` registry runs THIN CI — the heavy build/test jobs are skipped via a changed-files classifier job. `traceable-reqs.toml` has no Rust/product consumer; its load-bearing correctness gate is traceability. Invariants: the traceability gate ALWAYS runs (doc/registry tags and activated-stage coverage are load-bearing evidence, and a run with zero checks is unmergeable); any docs-site/ change runs the PR lint/unit/traceability lane; any other non-Markdown path runs that same lane. Main pushes also run thin CI only; the full evidence for main's exact SHA is the uncancelled golden run that produced it, and main advances only by fast-forward to that tested SHA. The classifier is plain git diff over the PR merge commit (HEAD^1..HEAD, fetch-depth 2) — no third-party changed-files action on the self-hosted runners. Skipped-required-check note: GitHub treats an if-skipped job as satisfying required status checks, and the classifier + traceability always report, so thin PRs stay mergeable.
2026-09-06T13:09:01.9931433Z - Required stages: impl
2026-09-06T13:09:01.9931569Z 
2026-09-06T13:09:01.9931660Z ### REQ-GOLDEN-CI-LANE
2026-09-06T13:09:01.9934065Z - Title: Builder PRs run only the thin lint/unit/traceability lane; the full cross-platform suite runs once on the gater-assembled golden branch, never cancels in progress, and main advances only to that exact tested SHA. Both coordinated two-host jobs run by default on every golden branch and workflow_dispatch; manual dispatch exposes an explicit twohost opt-out for intentional evidence isolation, never an opt-in whose omission can silently skip the seam. Their dependency on the suite is a rendezvous barrier only: !cancelled() runs both roles after either suite result so an unrelated red cannot silently erase independent seam evidence. A checked-in flake registry may authorize exactly one same-SHA rerun for an exact test id with recorded evidence and a retirement condition; a rerun red blocks the batch. workflow_dispatch remains available for an explicit candidate SHA. (ADR-0050; ACCESS-CONTROL W0.)
2026-09-06T13:09:01.9936503Z - Required stages: doc, impl
2026-09-06T13:09:01.9936653Z 
2026-09-06T13:09:01.9936752Z ### REQ-FLAKE-LEDGER-KEYED-ROWS
2026-09-06T13:09:01.9943440Z - Title: releases#95: `docs/FLAKE-LEDGER.md` rows are keyed by the TEST (or CI-step) NAME, never by a hand-numbered position column. The number column made an append-at-end table into a merge hazard — two lanes each appending "row N+1" conflict textually AND mint the same N — but the harder half is that a POSITIONAL key can be squatted: commit 5199464 (2026-07-15) minted the citation `FLAKE-LEDGER #15` for the HEAVY-AT-BIRTH classification ruling WITHOUT adding a ledger row (the table was 14 rows before and after), the label then propagated to 20 further sites across `.config/nextest.toml`, six test-file headers, two triage docs and this registry, and commit e1a3338 (2026-08-01) appended a genuinely unrelated row 15 (the servicehost pid-identity defect) — so every in-tree `#15` citation silently resolved to the wrong entry until this change. The fix is therefore BOTH halves in one commit: drop the column AND re-key every citation, in-table and out. In-table backrefs (rows citing #7/#8/#10/#12/#13 in their own prose) become test names; external citations become `FLAKE-LEDGER: <test name>`; the phantom `#15` class is re-pointed at what it actually names — the HEAVY-AT-BIRTH ruling, whose canonical text is the `.config/nextest.toml` <HEAVY> stanza, cited by ruling name and not by any number. Gate: doc — the ledger states the keying discipline, carries no number column, and BACKFILLS the row the phantom number concealed. That backfill was deferred to doyle at first report and handed back (2026-08-02): the primary RCA log predates doyle's last context reset and is not retained, so the 20 citation sites plus `5199464`'s own commit message — which ends "Ledger entry appended." over a diff that touches only `ci.yml` and `.config/nextest.toml` — ARE the surviving record. The row is therefore written in date order rather than appended, and states in its own status column that it is reconstructed and which evidence (run ids, per-sighting timestamps, raw gate logs) did not survive; a backfill indistinguishable from a contemporaneous row would be cited as if it carried primary evidence. The ledger intro carries that backfill discipline as a standing rule. Kin REQ-HEAVY-UNIT-CLASSIFICATION, REQ-CI-WINDOWS-PHASE-A-BOUND (both cite the ledger).
2026-09-06T13:09:01.9949465Z - Required stages: doc
2026-09-06T13:09:01.9949596Z 
2026-09-06T13:09:01.9949700Z ### REQ-RELEASE-VERSION-TAG-ASSERT
2026-09-06T13:09:01.9956410Z - Title: releases#106: the release workflow REFUSES to publish a release whose built binary disagrees with the tag. `.github/workflows/release.yml` already guards the CHANGELOG against the tag (`ver="${GITHUB_REF_NAME#v}"`, `::error::` + `exit 1`) but nothing proves the ARTIFACT carries that version — a tag pushed without bumping `[workspace.package] version` (and the first-party `spt-*` lines in `Cargo.lock`) ships a binary that self-reports the previous release. Shape: after the platform builds and BEFORE `gh release create`, execute the freshly built binary FOR THE RUNNER'S OWN PLATFORM with `--version`, compare to `${GITHUB_REF_NAME#v}`, and on mismatch fail with a named error stating BOTH values and naming the fix. SCOPE CONSTRAINT, load-bearing: a cross-compiled `matrix.target` artifact CANNOT be executed on the runner, so the assert must sit where a natively-runnable artifact is in scope — the native (non-`matrix.target`) leg, or the notes job after artifact download if the native asset is fetchable there. The assert lands in the RELEASE workflow, never as a new CI window (two-runs-per-sha ruling). SHIPPED SITE: the `assemble` job after `download-artifact` and before the docs bundle and `gh release create` — the one place the whole asset set exists at once on a box that can natively execute part of it, and the bytes there are the ones about to be published rather than a build-tree lookalike. The two Linux assets are covered; the WINDOWS asset is not, stated rather than glossed — no Linux runner can execute it, and a matching Linux artifact is evidence the workspace version was bumped, not evidence the Windows leg built from that tree. VERIFICATION STANCE, recorded at mint and honored: the workflow is tag-triggered and cannot be run locally, so no green is claimed for it. Instead the logic was EXTRACTED to `.github/ci/release-version-assert.sh` (the repo's own convention for CI logic, beside `ci-notify.sh` and the reap-census pair) precisely so it stops being unrehearsable, and its selftest runs on every golden Linux leg. Gate: impl — the script plus release.yml calling it; unit — `release-version-assert-selftest.sh`, 12 assertions, over half of them negative controls (stale binary, binary ahead of tag, missing artifact, unrunnable binary, empty asset list, absent tag).
2026-09-06T13:09:01.9962516Z - Required stages: impl, unit
2026-09-06T13:09:01.9962669Z 
2026-09-06T13:09:01.9962770Z ### REQ-CI-NOTIFY-CANCELLED-CLASS
2026-09-06T13:09:01.9968267Z - Title: releases#107: the CI notifier classifies CANCELLED separately from FAILURE, so a cancelled golden run stops paging the fleet as a red. `.github/ci/ci-notify.sh` `check()` (lines 94-111) folds `failure | cancelled` into one `failing` string and renders `verdict="FAILURE"` for any non-empty value. PRECEDENCE IS THE WHOLE RISK: a genuine failure normally CANCELS its siblings, so mixed failure+cancelled IS the shape of a real red — ANY `failure` present ⇒ FAILURE verdict (cancelled jobs may still be listed as context); ONLY zero failures AND ≥1 cancelled ⇒ the cancelled class, worded so it does not contain the word FAILURE. `skipped` stays non-red (REQ-CI-DOCS-ONLY-THIN deliberately skips jobs on docs-only PRs). Getting the precedence backwards silences real reds. Gate: impl — the split classes plus a LIBRARY seam, because the script is untestable as written (it `exit 0`s early when `spt` is absent from PATH and otherwise proceeds to real sends). Minted proposing a `CI_NOTIFY_DRY_RUN` env knob; SHIPPED as `SPT_CI_NOTIFY_LIB=1`, which sources the file for its `compute_verdict`/`compose_body` functions and returns before the probe/recipient/send sections — the seam `reap-census.sh` already established with `SPT_CI_REAP_LIB`, and it exposes the BODY as well as the verdict, which the wording half of this requirement needs. Executing (rather than sourcing) with the flag set exits 2 rather than no-opping to 0. unit — a `ci-notify-selftest.sh` driving the RESULT_* matrix (all-success / single-failure / all-cancelled / failure+cancelled / skipped-only / never-ran) and asserting both the verdict and the rendered body, wired into golden.yml the way `.github/ci/reap-census-selftest.sh` is. The selftest MUST carry a POSITIVE CONTROL — that a real FAILURE still pages — not merely that cancelled goes quiet. Kin REQ-CI-DOCS-ONLY-THIN, REQ-GOLDEN-CI-LANE.
2026-09-06T13:09:01.9973335Z - Required stages: impl, unit
2026-09-06T13:09:01.9973482Z 
2026-09-06T13:09:01.9973583Z ### REQ-GOLDEN-RESPIN-TEST
2026-09-06T13:09:01.9978536Z - Title: BAROMETER (releases#105; operator-ruled 2026-08-02): a defect found on an ASSEMBLED golden head is triaged by two questions answered separately, in order, Q2 never feeding Q1. Q1 RESPIN classifies by WHERE THE DEFECT LIVES, never by which files the fix touches: a defect inside what golden validated (code, build config, tests, CI config, gated docs) invalidates the head's evidence and buys a respin — the refusal is the DEFAULT; a defect outside golden's evidence ships, and the fix re-homes to buy its evidence on the head it lands with, which is never merge-untested since everything still lands ff-only via a future golden head. The shortcut is earned only against the PROVABILITY BAR: a name-only diff confined to non-code PLUS the compiled-in exceptions (version material) checked by MEASUREMENT — provable by diff, not plausible by argument; a delta needing an argument buys the full window. Q2 SEVERITY decides urgency and sizing, never which sha carries the fix. Carries two riders of the same root: the PUSH run IS the golden run (golden.yml two-host condition at 598/677 — a push satisfies `event != workflow_dispatch` and runs both legs unconditionally, so a reflexive dispatch is a duplicate serial window), and a superseding ruling must land where the reader looks before any actor carries the stale sentence into a plan (releases#71 5153399070 -> 5155301974). Amends ADR-0050 and RELEASE-RUNBOOK step 1: tested-SHA-equals-shipped-SHA becomes tested-SHA-equals-shipped-SHA-OR-provable-code-free-delta-from-a-tested-SHA. Worked example v0.51.0 counter 86 @6312b01: five files, zero .rs, 92 version-sensitive tests green on the bumped tree.
2026-09-06T13:09:01.9983360Z - Required stages: doc
2026-09-06T13:09:01.9983484Z 
2026-09-06T13:09:01.9983594Z ### REQ-HAZARD-REDISPATCH-CONTROL-STEAL
2026-09-06T13:09:01.9987094Z - Title: REDISPATCH-TRUTH W1 (KNOWN-HAZARDS 7.41, hertz field RCA 2026-07-16 — 4/5 endpoints frozen per brain cycle): a fresh dispatcher must NEVER re-serve a terminal stream — a replayed historical Attach must not steal (same-identity silent become_controller, no Displaced) or clear (replayed-EOF detach_session) a LIVE controller. The legitimate same-by successor re-take after a brain restart still silently re-takes: the discriminator is stream LIFECYCLE, never origin identity. Gate: int — production-path regression D1: finished historical Attach + current active Attach, same endpoint/origin; restart target brain only (real run_dispatch_loop rediscovery, NO manual re-serve — the pre-fix e2e bypass is the lesson); prove the historical stream neither takes nor clears the current controller and current input/output stays exactly-once without detach; doc — KNOWN-HAZARDS 7.41. HEAVY nextest group at birth (the HEAVY-AT-BIRTH ruling, .config/nextest.toml). Kin REQ-REDISPATCH-FINISHED-RETIRE (the mechanism), REQ-BRAIN-RESUME-NO-CONTROL-STEAL (the CLOSED session-cursor sibling — different leg), ADR-0038.
2026-09-06T13:09:01.9990490Z - Required stages: doc, int
2026-09-06T13:09:01.9990634Z 
2026-09-06T13:09:01.9990748Z ### REQ-REDISPATCH-FINISHED-RETIRE
2026-09-06T13:09:01.9994034Z - Title: REDISPATCH-TRUTH W1 (ADR-0038, hertz fix A): finished/terminal stream rows are RETIRED from redispatch eligibility — NetShared.streams today has NO removal path (single insert nethost.rs ~649; StreamLog::finish only marks) so every dispatcher generation re-enumerates every historical stream forever. Retire terminal rows from the enumeration the dispatcher claims from (remove, or lifecycle-exclude), preserving only the post-EOF state genuinely needed by other readers (presence/log reads); bounded growth replaces forever-discoverable rows. Clearing the whole table on brain restart is REJECTED (destroys live streams' reconstruction facts). Gate: impl — the retirement path; unit — a finished stream is invisible to the dispatch enumeration while an active one stays claimable + post-EOF reader state survives retirement; doc — rides ADR-0038 + the triage doc. Kin REQ-HAZARD-REDISPATCH-CONTROL-STEAL (the invariant it satisfies), REQ-STREAM-OPENER-DURABLE.
2026-09-06T13:09:01.9996643Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:01.9996809Z 
2026-09-06T13:09:01.9996914Z ### REQ-STREAM-OPENER-DURABLE
2026-09-06T13:09:02.0000793Z - Title: REDISPATCH-TRUTH W1 (ADR-0038, hertz fix B): stream classification identity is RESTART-DURABLE and independent of the evictable data ring — today reconstruction classifies via peek_first_line from ring seq 0, but StreamLog is a bounded 4096-transport-chunk ring (DEFAULT_STREAM_RING_CHUNKS, nethost.rs:111), so a high-traffic ACTIVE stream's opener/Request record evicts and replay classifies Unknown/Failed = active stream permanently abandoned. Pin an immutable bounded opener/classification fact (the complete first NDJSON record, or its derived family+cursor facts) OUTSIDE the data ring per broker-held inbound stream, held until stream close; recovery classifies from that metadata, never ring seq 0. Ring semantics and size untouched (enlarging the ring for a correctness fact is REJECTED). Gate: impl — the pinned opener fact + classification cutover; unit — classification survives full ring roll-over; int — production-path regression D2: push >4096 transport chunks on an active Attach (opener evicted), restart dispatcher, prove durable classification resumes the SAME operator stream. HEAVY nextest group at birth. Kin REQ-DISPATCH-CLAIM-RETRY, REQ-HAZARD-REDISPATCH-CONTROL-STEAL.
2026-09-06T13:09:02.0004367Z - Required stages: impl, unit, int
2026-09-06T13:09:02.0004523Z 
2026-09-06T13:09:02.0004629Z ### REQ-DISPATCH-CLAIM-RETRY
2026-09-06T13:09:02.0007645Z - Title: REDISPATCH-TRUTH W1 (ADR-0038, hertz fix C): dispatcher claims are RETRYABLE and outcomes are CLASSIFIED — today claimed.insert(stream_id) happens PRE-spawn (dispatch.rs:206/214) and is never cleared or retried on Unknown/Failed, so one transient worker-setup failure permanently abandons the stream. Distinguish active/retryable vs served vs finished: a transient worker-setup failure releases/requeues the claim (bounded/backoff-shaped); terminal classification outcomes stay terminal and do NOT hot-loop (show the distinction in test, not just code). Gate: impl — claim lifecycle; unit — transient failure requeues + terminal outcome does not (no hot-loop under a persistently-failing stream); int — production-path regression D3: inject ONE transient worker-start failure, prove claim retry recovers the stream with NO duplicate controller/output. HEAVY nextest group at birth. Kin REQ-REDISPATCH-FINISHED-RETIRE, REQ-STREAM-OPENER-DURABLE.
2026-09-06T13:09:02.0010447Z - Required stages: impl, unit, int
2026-09-06T13:09:02.0010654Z 
2026-09-06T13:09:02.0010764Z ### REQ-HAZARD-MESH-BOOTSTRAP-TRAP
2026-09-06T13:09:02.0014565Z - Title: MESH-RECOVERY W1 (KNOWN-HAZARDS 7.42, hertz field RCA 2026-07-10 — HFENDULEAM+ENLYZEAM symmetric green-status sequester): a node holding a valid RosterEntry.address for a peer is NEVER route-less — a failed dial must not delete the only bootstrap route, and recovery must never require an already-successful connection or operator state surgery. Today: resolve_submit_addr = exact cache else id-only (never roster), PRESENCE_DIAL_FAILED unconditionally drop_seed's the cache row, and the cache refills only after a successful seed-proof exchange — one transient + stalled discovery = self-sustaining isolation, invisible (net_up true, heartbeat fresh, durable counts normal). Gate: int — production-path regression at the REAL pump resolver/failure-lifecycle seam: valid roster + matching cache, ONE transient dial failure, id-only discovery DISABLED, prove the next attempt still holds the roster-derived route AND all-peer-fail-then-restore converges with zero state surgery; doc — KNOWN-HAZARDS 7.42. HEAVY nextest group at birth if it spawns a daemon tree. Kin REQ-PEER-ROUTE-CHAIN (the mechanism), REQ-CONV-1 (the falsified drop-on-fail predecessor), ADR-0039.
2026-09-06T13:09:02.0017811Z - Required stages: doc, int
2026-09-06T13:09:02.0017953Z 
2026-09-06T13:09:02.0018053Z ### REQ-PEER-ROUTE-CHAIN
2026-09-06T13:09:02.0022161Z - Title: MESH-RECOVERY W1 (ADR-0039, RCA wave 1): dial-address resolution is the ROUTE CHAIN — exact peer-cache entry, then VALIDATED RosterEntry.address (address.id must match the peer key; a poison row never becomes a route), then id-only discovery — always fully consulted in order (no failure-count heuristics gating legs; rotation machinery REJECTED). Retention is NONDESTRUCTIVE: PRESENCE_DIAL_FAILED demotes the cached route to suspect (skipped in favor of the roster leg while suspect, superseded by any validated fresher address from connect write-back or reconcile), never deletes a sole route; removal only via validated-fresher replacement or roster tombstone. Validated roster addresses RECONCILE into the cache at daemon startup and on roster merge (beyond gapfill's fill-only: validated-fresher replaces failed/suspect rows) — recovery is connection-independent. Amends REQ-CONV-1's drop-on-fail mechanism; peeraddrs.rs/pump doc-comment truth rides the same change. Gate: impl — chain + demote + reconcile; unit — chain order incl. id-mismatch roster row resolves nothing + suspect row survives N failures with no replacement + reconcile replaces suspect with validated-fresher; int — rides REQ-HAZARD-MESH-BOOTSTRAP-TRAP D-legs; doc — ADR-0039 + CONTEXT peer-route chain entry. Kin REQ-PEERADDR-INVARIANT, REQ-CONV-1.
2026-09-06T13:09:02.0025811Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0025982Z 
2026-09-06T13:09:02.0026079Z ### REQ-PEERADDR-INVARIANT
2026-09-06T13:09:02.0029369Z - Title: MESH-RECOVERY W1 (ADR-0039, RCA wave 2): the peer-addrs cache INVARIANT — outer peer key == address.id — is ENFORCED on load and on write: invalid rows are repaired from the current roster when possible, rejected (dropped loudly) otherwise; never silently kept, never used as a route. MIGRATION = rebuild invalid rows from roster on first post-upgrade load; bare-deleting peer-addrs.json is REJECTED (cold recovery depends on the id-only path staying BEHIND warm routes — nuking every warm route trades one trap for another). gapfill_peeraddrs and PeerAddrStore::put stop accepting mismatched mappings (the live 5ff…-outer poison-row class on both incident nodes). Absent/corrupt-degrades-empty behavior untouched. Gate: impl — load/write enforcement + repair + migration; unit — mismatch rejected on put, repaired-or-dropped on load, valid rows untouched by migration, gapfill refuses a mismatched roster entry; doc — ADR-0039. Kin REQ-PEER-ROUTE-CHAIN, REQ-MESH-2 (gapfill), REQ-CONV-1.
2026-09-06T13:09:02.0032212Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.0032368Z 
2026-09-06T13:09:02.0032464Z ### REQ-PUMP-STAGE-TRUTH
2026-09-06T13:09:02.0036223Z - Title: MESH-RECOVERY W1 (ADR-0039, RCA wave 3 — the acceptance surface, same contract): peer-failure telemetry is STAGE-SPLIT and health is USER-MEANINGFUL. The single 10s PUMP_PEER_FAIL token splits into attributed stages — address-resolution, QUIC connect, ALPN, seed-proof send, seed-proof receive/verify, roster exchange — each failure stamped (wall+mono) and peer-attributed (subsumes the 2026-07-14 PUMP_PEER_FAIL-unstamped seed). daemon status / subnet status report: live peer count, last successful peer dial, last admitted registry update, duration of any all-peer failure; the incident fingerprint (all dials failing + heartbeat fresh + net_up true) MUST render degraded — no green without real peer progress. New fields ADDITIVE (N-1 readers unaffected). Gate: impl — stage split + status surfaces; unit — stage classification + health state machine (degraded on all-peer failure, healthy only on real progress, not on heartbeat/time); int — health flips degraded/healthy across a real peer outage/restore; doc — reference regen (CLI surface change → xtask gen, no internal codes in clap help). Kin REQ-PEER-ROUTE-CHAIN, REQ-DAEMON-5 (heartbeat — answers liveness, not reachability), REQ-CLI-2/REQ-SUBNET-8 (render legs).
2026-09-06T13:09:02.0039709Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0039881Z 
2026-09-06T13:09:02.0039986Z ### REQ-HAZARD-REDISPATCH-STALL
2026-09-06T13:09:02.0043892Z - Title: REDISPATCH-STALL W1 (KNOWN-HAZARDS 7.43, hertz v0.34 field RCA 2026-07-16 — recurrent 20-30s PTY/RC freezes, 17-62s DISPATCH tails): one wedged stream subscriber must NEVER stall stream serving, and recovery machinery must not manufacture new replay victims. Today: claim retries x the broad Err(_) opener fallback (dispatch.rs:414) install throwaway peek subscribers whose StreamLog::attach replays the entire retained ring UNDER the per-stream mutex with discarded write errors and the poisoned subscriber left installed — serial 15s bounded-write poison windows (33 observed, all 15,000-15,154ms) composing into the field stalls. Gate: int — production-path regression at the REAL run_dispatch_loop + StreamLog + serve_attach seams: wedge one subscriber conn, prove producer appends and unrelated streams stay flat while the poisoned subscriber is removed and the stream recovers (no abandonment); doc — KNOWN-HAZARDS 7.43. Binding: redispatch D1/D1b stay green every leg. HEAVY nextest group at birth. Kin REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE + REQ-DISPATCH-FALLBACK-CIRCUIT (the mechanisms), REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK (the deadline that fires), ADR-0038 Amendment.
2026-09-06T13:09:02.0047237Z - Required stages: doc, int
2026-09-06T13:09:02.0047374Z 
2026-09-06T13:09:02.0047485Z ### REQ-DISPATCH-FALLBACK-CIRCUIT
2026-09-06T13:09:02.0051357Z - Title: REDISPATCH-STALL W1 (ADR-0038 Amendment, fixes 1+5): the opener ring-peek fallback fires ONLY on the explicit UnsupportedVerb/old-broker answer — transport timeout/EOF/poison classify Failed and requeue bounded, NEVER a second replay subscriber (today first_line's Err(_) arm at dispatch.rs:414 catches everything; the comment intends old-broker-only). Retries are CLASSIFIED: pre-setup transient may retry; a deadline-poisoned replay is CIRCUIT-BROKEN (global backoff) and a replacement subscriber is never installed until the prior subscriber is fully gone. NOT a revert to the v0.33 burn-the-claim abandonment (rejected): the stream must recover after the breaker window — show it in test. Gate: impl — narrowed fallback arm + breaker; unit — timeout/EOF/poison never reach the peek path while unsupported-verb does + breaker trips and resets + no-reinstall-until-gone; int — T2 (no peek subscriber created on transport errors) + T5 (breaker recovery, the not-abandonment discriminator) + T7 mixed-image N-1 with REAL traffic-carrying streams (handshake-only insufficient); doc — ADR-0038 Amendment. Kin REQ-DISPATCH-CLAIM-RETRY (upgraded, not reverted), REQ-STREAM-OPENER-DURABLE (the N-1 window it narrows).
2026-09-06T13:09:02.0054945Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0055105Z 
2026-09-06T13:09:02.0055221Z ### REQ-STREAMLOG-SUBSCRIBER-DISCIPLINE
2026-09-06T13:09:02.0059550Z - Title: REDISPATCH-STALL W1 (ADR-0038 Amendment, fixes 2+3+4): StreamLog subscriber write discipline — (a) replay and live fan-out HALT at the first failed subscriber write and the failed subscriber is REMOVED, at ALL sites (attach replay, append, finish; PresenceLog mirrors ride the same change — today let _ = sub.write() discards errors and iteration continues, nethost.rs:322-346); (b) subscriber I/O moves OFF the StreamLog mutex: bounded per-subscriber writer queue, enqueue-under-lock / I/O-outside, overflow = detach + resume-from-cursor (the existing gapless-resume contract), never a producer stall; (c) attach worker completion + forwarding BOUNDED and cancelable — a poisoned brain subscriber cancels its paired serve/wire worker, no orphan forwarding legs (COORDINATE with DAEMON-LIFECYCLE C2 leases at build — same neighborhood, build once). Gate: impl — halt+remove all-sites + writer queue + cancelable pairing; unit — halt-at-first-failure removes the subscriber at each site + queue overflow detaches with cursor intact + producer append never blocks past enqueue; int — T1 (poisoned-replay halt) + T3 (producer latency flat under a wedged subscriber) + T4 (paired worker canceled on poison); doc — ADR-0038 Amendment. Kin REQ-HAZARD-REDISPATCH-STALL, REQ-CONN-POISON-ATTRIBUTION (the telemetry that caught it), REQ-EP-4 (PresenceLog contract unchanged above the write leaf).
2026-09-06T13:09:02.0063333Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0063503Z 
2026-09-06T13:09:02.0063612Z ### REQ-SERVE-OWNERSHIP-GENERATION
2026-09-06T13:09:02.0066883Z - Title: REDISPATCH-STALL W1 (ADR-0038 Amendment, fix 6): terminal-exclusion enforced PRE-SERVE + ownership/generation validation on attach/detach — a stale worker can never detach or displace a REPLACEMENT controller (today detach_if compares Arc ptr identity only; the serve path re-checks nothing at completion). Covers the UNFINISHED-stale-row control-steal shape (raw-close no-FIN viewports, emphasys C2 leak class feeding it) that finished-row retirement (D1/D1b) definitionally cannot see — the discriminating field observable on the next live steal catch = the stolen row's finished+retired flags. Gate: impl — pre-serve terminal exclusion + generation/ownership tokens on attach/detach; unit — stale-generation detach refused while the same-generation detach lands; int — T6 (UNFINISHED-stale attach row + live current controller + dispatcher restart: neither takes nor clears the replacement, D1/D1b green alongside); doc — ADR-0038 Amendment. Kin REQ-HAZARD-REDISPATCH-CONTROL-STEAL (the finished sibling), REQ-REDISPATCH-FINISHED-RETIRE.
2026-09-06T13:09:02.0069924Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0070096Z 
2026-09-06T13:09:02.0070200Z ### REQ-DISPATCH-HYGIENE-TELEMETRY
2026-09-06T13:09:02.0073330Z - Title: REDISPATCH-STALL W1 (ADR-0038 Amendment, fixes 7+8+9): dispatcher hygiene + keyed observability — (a) bounded redispatch worker pool with batched cold enumeration, NO claim locks held during I/O (today thread::spawn per stream, unbounded on a cold table); (b) sessions-lock discipline: clone-then-drop before detach_if/info.json I/O (KH 7.12 kin — no fs/conn I/O under global locks); (c) keyed stage telemetry: gen/stream/family/endpoint/attempt/conn on every dispatch record + replay/poison/cancel/worker-outcome events + gauges + PTY high-water/RC cursor — the field-discriminator surface (poison-window census, stream-sub-attach per generation dropping to O(active streams)). Gate: impl — pool + enumeration batching + lock discipline + telemetry keys; unit — pool bound honored under a cold flood + no-lock-across-I/O seam + telemetry key completeness; doc — ADR-0038 Amendment. Kin REQ-HAZARD-REDISPATCH-STALL, KH 7.12 (locks), REQ-CONN-POISON-ATTRIBUTION (extends its attribution).
2026-09-06T13:09:02.0076079Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.0076279Z 
2026-09-06T13:09:02.0076375Z ### REQ-BRAIN-READY-WINDOW-OBSERVABLE
2026-09-06T13:09:02.0082227Z - Title: THE BRAIN_UP -> brain.ready WINDOW IS OBSERVABLE IN ONE GREP (RCA-242-R2-LINUX, ruled 2026-08-29). Every happy-path stretch between the BRAIN_UP line and the ready file used to be SILENT: announce_coordinator_image's Ok(true) arm is empty, resume_session_cursors' Ok(_) arm is empty AND is the always-taken one (the supervised daemon brain hosts no PTY sessions), and write_ready's first current_exe_hash() reads and SHA-256s the whole binary inline before the atomic write. THE COST WAS PAID TWICE: four golden cells burned a full untuned 30s readiness budget while their own daemon stderr showed BRAIN_UP at 26-65ms, and the stall point -- blocked on a broker reply READ versus inside the exe-hash -- could not be separated from the log at all, so the RCA closed UNDISCRIMINATED by construction. activity_link_push_e2e then failed again WARM, which carved it out of the load-tail ruling and made the undiscriminated window gate-blocking rather than a footnote. THE SHAPE IS ONE TOKEN FAMILY WITH PER-PHASE ELAPSED MS: three BRAIN_PHASE lines whose figures SUM TO THE GAP, so a reader names the stretch in one read instead of a second instrumented run. ELAPSED, NEVER WALL STAMPS -- CONN_LIFECYCLE already carries wall_ms for correlation and a second clock in one log is how two clocks start disagreeing. THE READY LINE ALSO CARRIES THE RESOLVED PATH (gate rider): a test polls a path it resolved itself, so a poller/writer mismatch is ruled in or out rather than inferred. THE HASH FIGURE IS TIMED WHERE IT IS PAID, INSIDE write_ready, AND CARRIES computed=true|false. This is the requirement, not an implementation detail: current_exe_hash memoises in a OnceLock, so an instrument that timed a SECOND call from the caller would measure a cache hit and report ~0ms with computed=false on every run -- a vacuous meter for the one stretch it exists to measure, and the failure would be invisible because 0ms reads as good news. EMISSION IS emit_line_err! THROUGHOUT so these breadcrumbs never become the next census's residue. SCOPE BOUNDARY, RULED: moving current_exe_hash OFF the ready path is the honest fix for that stretch and is NOT part of this requirement -- it touches ADR-0018 Q7's ratified readiness semantic and REQ-BRAIN-HASH-ONCE's resident-bytes truthfulness contract, and rides its own board request.
2026-09-06T13:09:02.0088098Z - Required stages: impl, unit
2026-09-06T13:09:02.0088247Z 
2026-09-06T13:09:02.0088342Z ### REQ-BRAIN-HASH-ONCE
2026-09-06T13:09:02.0091441Z - Title: REGISTRY-LIFECYCLE W1 (ADR-0040 rider; dropped THE-FORKENING W4 rider escalated — hertz re-measured live 2026-07-17: 61.29 MiB/s predicted vs 63.91 observed, 15.5%/core): the brain executable self-hash is captured EXACTLY ONCE per brain process (OnceLock in run_brain before the heartbeat loop); every write_ready reuses the cached value; failed capture stays None with no per-tick retry; current_exe_hash doc-comment corrected same commit. Once-at-start capture IS the resident-bytes truthfulness contract: the per-tick PATH re-read published the NEW file hash from a resident-OLD-bytes brain post-swap (breadcrumb lie in the enlyzeam class it exists to catch). 500ms ready-write cadence unchanged. Gate: impl — cached capture; unit — injected digest-counter==1 across initial+N heartbeat publishes with pid/generation/hash stable, fresh process fixture computes independently; existing D7 process-replacement e2e retained green (new brain publishes new hash first write).
2026-09-06T13:09:02.0094121Z - Required stages: impl, unit
2026-09-06T13:09:02.0094379Z 
2026-09-06T13:09:02.0094537Z ### REQ-ONEWAY-STREAM-TERMINAL
2026-09-06T13:09:02.0096813Z - Title: REGISTRY-LIFECYCLE W1 (ADR-0040 decision 1, hertz defect B leg 1): a one-way fire-and-forget stream family is TERMINAL at successful FIN, sender-side — the registry pump retires its OWN feed row after write+FIN via the existing net-stream-retire verb (best-effort on N-1 brokers per ADR-0038 A). Sender history on the long-lived pump conn stops accumulating: steady-state row population is O(active exchanges), not O(feeds since conn start). Gate: impl — pump push_feed retire-after-FIN; unit — successful feed retires its row, failed/unFINed feed does not, retire failure is best-effort non-fatal; int — rides REQ-HAZARD-REGISTRY-STALL plateau seam (eligible rows plateau O(active) over N rounds); doc — ADR-0040.
2026-09-06T13:09:02.0098854Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0099108Z 
2026-09-06T13:09:02.0099212Z ### REQ-STREAM-INFOS-SERVER-FILTER
2026-09-06T13:09:02.0101128Z - Title: REGISTRY-LIFECYCLE W1 (ADR-0040 decision 2, hertz defect B leg 2): broker stream_infos excludes initiated_locally rows SERVER-SIDE (retired already excluded) before cloning/serializing to the dispatcher — the O(history) IPC enumeration cost dies at the source. Dispatcher keeps its client-side guard (double-filter harmless; N-1 compatible both directions: older dispatcher skips those rows anyway, older broker just keeps the old cost). Gate: impl — server-side filter; unit — local rows absent from the reply while peer-initiated rows with bytes remain, opener/re-attach enumeration semantics unchanged.
2026-09-06T13:09:02.0102910Z - Required stages: impl, unit
2026-09-06T13:09:02.0103055Z 
2026-09-06T13:09:02.0103164Z ### REQ-REGISTRY-APPLY-TRANSACTIONAL
2026-09-06T13:09:02.0106089Z - Title: REGISTRY-LIFECYCLE W1 (ADR-0040 decision 3, hertz defect B leg 3): registry feed application is TRANSACTIONAL per feed — serve_registry_feed merges decoded labels + instance updates in memory across transport chunks and applies ONCE at EOF (or one bounded batch commit for oversized feeds); write_snapshots runs O(feeds), never O(chunks x record-kinds); attention-shift side effects fire once post-merge. No synchronous full-registry rewrite inside a per-chunk drain iteration (KH 7.12/7.43 discipline on the brain side — the per-chunk rewrites are what stalled IPC drain and manufactured the 15s seat-writer poisons). Gate: impl — accumulate-then-apply; unit — snapshot-write counter across a multi-chunk feed == 1 (bounded batches: == ceil(records/batch)), merge result equals per-chunk semantics, gate policy still applied per record; int — rides REQ-HAZARD-REGISTRY-STALL seam (snapshot writes O(feeds), zero poisons); doc — ADR-0040.
2026-09-06T13:09:02.0108632Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0108800Z 
2026-09-06T13:09:02.0108908Z ### REQ-REGISTRY-REPLAY-BOUNDED
2026-09-06T13:09:02.0111927Z - Title: REGISTRY-LIFECYCLE W1 (ADR-0040 decision 4, hertz defect B leg 4, defense-in-depth behind REQ-ONEWAY-STREAM-TERMINAL): a deadline-poisoned one-way (Registry) replay carries a per-stream strike budget; at budget the row retires TERMINAL with loud telemetry (DISPATCH_EV event). Safe by family semantics: a feed is an idempotent snapshot advertisement, the next pump round (~30s) re-advertises — worst-case loss is one advertisement round, already the family freshness unit. Request/reply families keep ADR-0038 Amendment circuit-breaker semantics — terminal-at-budget is ONE-WAY-FAMILY-ONLY (never a revert toward burn-the-claim abandonment for request streams). Gate: impl — strike budget + terminal retire, one-way-scoped; unit — poison strikes accumulate per stream, budget retires terminal + emits, request/reply family unaffected by the budget path; int — poisoned registry replay stops redispatching after budget while fresh feeds keep serving.
2026-09-06T13:09:02.0114588Z - Required stages: impl, unit, int
2026-09-06T13:09:02.0114742Z 
2026-09-06T13:09:02.0114846Z ### REQ-SEAT-LIFETIME-BOUNDED
2026-09-06T13:09:02.0118151Z - Title: REGISTRY-LIFECYCLE W1 (ADR-0040 decision 5, hertz defect B leg 5 CROSS-FAMILY per the final attribution correction — my_stream_subs releases only at conn-loop exit, NO unsubscribe verb existed, ~546/589 broker threads were unnamed parked SubscriberSeat writers on completed Registry/sync/update seats): new KIND_NET_STREAM_UNSUBSCRIBE broker verb removes the SubscriberSeat, stops AND JOINS its writer thread, drops its cursor; every dispatch worker calls it on serve completion (ALL families, success or failure); the retire sweep calls it for retired rows. N-1: unknown-kind on an older broker = tolerated send_error path, caller best-effort. Rider: spawned daemon threads NAMED (SubscriberSeat writer sub-writer-s<id> minimum, conn accept handlers) — field censuses attribute, not infer. Gate: impl — verb + worker/sweep call sites + thread naming; unit — unsubscribe removes seat/joins writer/drops cursor + idempotent + unknown-stream tolerated; int — rides REQ-HAZARD-REGISTRY-STALL seam (physical seat/thread counts plateau CROSS-FAMILY after completion); doc — ADR-0040.
2026-09-06T13:09:02.0121334Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0121501Z 
2026-09-06T13:09:02.0121604Z ### REQ-HAZARD-REGISTRY-STALL
2026-09-06T13:09:02.0124653Z - Title: REGISTRY-LIFECYCLE W1 (KNOWN-HAZARDS 7.44, hertz post-close v0.36 RCA — the umbrella conformance seam): streams and seats on a long-lived connection have BOUNDED lifetime. The hertz regression seam verbatim: real long-lived pump + dispatcher integration, N registry-only rounds over ONE persistent connection, asserting (a) dispatcher-eligible Registry rows plateau O(active) not O(N); (b) physical stream/subscriber/seat counts plateau after completion CROSS-FAMILY (sync/update seats included, not just Registry); (c) snapshot writes O(feeds) not O(chunks x record-kinds); (d) brain refresh produces ZERO historical Registry replay subscriptions; (e) zero CONN_WRITE_POISONED / replay-write-failed events; (f) broker thread count returns to a bounded baseline. HEAVY nextest group at birth (spawns a real daemon tree). Binding: redispatch D1/D1b + REDISPATCH-STALL T1-T7 + mesh-recovery legs green every leg (retire machinery + registry gate = substrate). Gate: int — the seam above; doc — KNOWN-HAZARDS 7.44.
2026-09-06T13:09:02.0127367Z - Required stages: doc, int
2026-09-06T13:09:02.0127510Z 
2026-09-06T13:09:02.0127606Z ### REQ-ENDPOINT-ONLINE-TRUTH
2026-09-06T13:09:02.0131198Z - Title: REGISTRY-LIFECYCLE W2 (ADR-0041 decisions 1+2, emphasys C1 P0): ONLINE is earned, not declared — cmd_listen stamps status=online only from actual persisted state + hosting authority, never from manifest psyche_init capability alone (no more ready_agent/controllable=false hybrid rows born online-authoritative); livehost reconcile SPLITS control cleanup (clear controlled/driven_by/viewer_count for EVERY endpoint absent from session truth, regardless of state/controllable) from offline classification (live_agent+controllable gate unchanged); legacy hybrid rows self-heal after a SUCCESSFUL broker query only (broker failure is never interpreted as an empty session set); terminal signoff/owner-loss = atomic CAS-guarded offline + ready/address removal WITHOUT overloading soft api session-end (/clear preserves the live listener). Gate: impl — creator gate + reconcile split + self-heal + terminal path; unit — creator refuses capability-only online, cleanup clears stamps on state-quirk rows, broker-failure never mass-offlines; int — dead-PID hybrid row does NOT survive a reconcile cycle (the immortal-row regression); doc — ADR-0041.
2026-09-06T13:09:02.0134396Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0134605Z 
2026-09-06T13:09:02.0134708Z ### REQ-STREAM-LEASE-CLASSES
2026-09-06T13:09:02.0138299Z - Title: REGISTRY-LIFECYCLE W2 (ADR-0040 decision 6 + ADR-0041, emphasys C2 P0): streams declare a lifetime class at open — RC attach/view streams are ConnectionBound (opener conn EOF means the target sees FIN, serve_attach runs detach_session, controller slot + CONTROLLED stamps clear — a dead viewer can never pin a controller across its own connection death; today the raw-closed viewport attach stream is restart-durable forever); inter-brain streams stay Durable (NEVER globally retire on Brain disconnect — brain-swap correctness depends on it). Late-close identity validated (stale opener A close cannot evict newer controller B — rides ADR-0038 Amendment fix-6 generation tokens + W1 seat teardown machinery; same neighborhood, built once per the standing C2 coordination ruling). Lifetime class = additive open field, absent = Durable (N-1 openers keep exact current semantics). Gate: impl — class at open + ConnectionBound EOF chain; unit — class routing + absent-defaults-Durable + late-close identity refusal; int — raw viewport close frees the controller full-chain incl. across broker restart, brain_swap/daemon_refresh/redispatch legs stay green; doc — ADR-0040/0041.
2026-09-06T13:09:02.0141782Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0141954Z 
2026-09-06T13:09:02.0142053Z ### REQ-REST-TERMINAL-NORMALIZE
2026-09-06T13:09:02.0145596Z - Title: REGISTRY-LIFECYCLE W2 (ADR-0041 decision 3, rest-normalize P0): definitive hosted-session loss normalizes TERMINALLY and ATOMICALLY — one store-level mutation writes status=offline + rest_state=suspended + clears dormant_since_ms in the SAME info.json write, invoked at authoritative broker-session-loss/liveness-reap + cmd_stop. NOT daemon_rest_event(Suspend) (no edge when effective already Suspended — raw Active intent survives); NOT reader-side blanket offline-implies-suspended (destroys explicit-Wake semantics: wake writes intent first, reconcile consumes). Graceful shutdown keeps echo-before-teardown. Kills the zombie WAKE_RESUME loop (perri field: repeated cross-generation resumes of a dead session). Gate: impl — atomic terminal-normalize mutation + call sites; unit — store-level atomic pair never mixed + endpoint_stop covers already-offline/raw-Active input; int — session vanish means offline+suspended and the NEXT reconcile emits NO WAKE_RESUME, explicit suspended/offline->Wake->Active launches EXACTLY once, RefuseLivePid-then-valid-bind custody race leaves the revived seat unsuspended; doc — ADR-0041.
2026-09-06T13:09:02.0148817Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0149065Z 
2026-09-06T13:09:02.0149169Z ### REQ-CONTROL-STAMP-CONVERGENCE
2026-09-06T13:09:02.0151279Z - Title: REGISTRY-LIFECYCLE W2 (ADR-0041 decision 4, emphasys C3 P1): control-stamp writes are session/generation-VALIDATED (or per-endpoint stamp transitions serialized) so a pre-reap KIND_SESSIONS snapshot can never relatch controlled=true after the exit-waiter reap; no fs I/O under global locks (KH 7.12); every no-session path clears all three control fields (controlled/driven_by/viewer_count). Gate: impl — validated/serialized stamp transitions; unit — stale-snapshot write refused after a newer no-session truth, no-session paths clear all three; int — poll-vs-reap interleave converges to cleared stamps (the relatch regression); doc — ADR-0041.
2026-09-06T13:09:02.0153211Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0153375Z 
2026-09-06T13:09:02.0153476Z ### REQ-HAZARD-ENDPOINT-LIFECYCLE
2026-09-06T13:09:02.0155711Z - Title: REGISTRY-LIFECYCLE W2 (KNOWN-HAZARDS 7.45 — the umbrella conformance seam for ADR-0041): endpoint lifecycle state converges to truth from EVERY death path. Regression matrix from the three hertz reports + operator field: dead-PID hybrid row does not survive reconcile; raw viewport close frees the controller (full chain, broker restart included — shared with REQ-STREAM-LEASE-CLASSES int); definitive death means offline+suspended atomically and the next reconcile emits no WAKE_RESUME; explicit Wake still launches exactly once; poll-vs-reap interleave converges to cleared stamps. HEAVY nextest group at birth for any leg spawning a daemon tree. Gate: int — the matrix; doc — KNOWN-HAZARDS 7.45.
2026-09-06T13:09:02.0157711Z - Required stages: doc, int
2026-09-06T13:09:02.0157854Z 
2026-09-06T13:09:02.0157946Z ### REQ-SPAWN-FRESH-TRUTHFUL
2026-09-06T13:09:02.0161629Z - Title: REGISTRY-LIFECYCLE W3 (ADR-0041 decision 5, spawn-fresh): endpoint create is TRUTHFUL at the broker, atomically — spawn policies split Fresh/RejectExisting vs ResumeOrReuse; new KIND_SPAWN_FRESH wire verb with TYPED conflict (distinct kind fails LOUD on an N-1 broker vs a silently-ignored serde-defaulted field), sharing the single-flight/PTY spawn body; Fresh at AlreadyLive/lost-race returns the typed conflict NEVER Spawned(existing); CLI contract exit 1 + stable ENDPOINT_CREATE_CONFLICT:<id> token + no attach + no state mutation + never kills the live session; omitted --resume keeps the documented create-default (fresh-and-conflict, same as explicit --create); preflight stays UX-only (cannot close TOCTOU). Gate: impl — policy split + verb + CLI contract; unit — intent-aware decision matrix; int — create-over-live E2E (exit 1, exact token, no attach, same broker PID) + concurrent-fresh race (one Created one Conflict, never two children, never Reused-as-success) + mixed fresh/resume race + offline/raw-Active+stale-ledger create + N-1 IPC loud-fail never-Created; doc — ADR-0041 + CLI reference regen (docs-drift gate: xtask gen, no internal codes in clap help).
2026-09-06T13:09:02.0164932Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0165103Z 
2026-09-06T13:09:02.0165203Z ### REQ-ENDPOINT-CYCLE-HONEST
2026-09-06T13:09:02.0167494Z - Title: REGISTRY-LIFECYCLE W3 (ADR-0041 decision 6, operator deployah stop/run wedge): cycle verbs share ONE liveness authority — the ALREADY_LIVE dup-guard liveness-probes the claimed session client tree before refusing (dead tree means reap + respawn honestly, never a refusal citing a zombie); the shutdown state machine consults the same source so is-it-live has one answer (no ALREADY_LIVE / list-OFFLINE / shutdown-NO_EDGE three-way contradiction on the same endpoint). Gate: impl — probing dup-guard + unified authority; unit — dead-tree claim probes and reaps, live claim still refuses; int — controlled zombie (killed client tree, surviving hosted record) leads to endpoint run succeeding honestly end-to-end; doc — ADR-0041.
2026-09-06T13:09:02.0170724Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0170967Z 
2026-09-06T13:09:02.0171106Z ### REQ-GOSSIP-PROJECT-DERIVE-ONCE
2026-09-06T13:09:02.0176055Z - Title: REGISTRY-LIFECYCLE W4 fast-follow (hertz v0.37.0 field-verify CPU-gate FAIL RCA 2026-07-17, doyle seam-verified same day; timing-pinned ~16s burst clusters per 30s Registry pump round): recent-project gossip derives a project id AT MOST ONCE PER DISTINCT CWD per advertisement round — recent_projects_for dedups the RAW cwd string BEFORE project_id_for_dir (today seen.insert(pid) dedups AFTER derivation at registryhost.rs:1001, so every duplicate-cwd sessions.log row pays a git remote/rev-parse spawn: ~200 derivations/round on a 12-perch box, the post-self-hash-fix CPU floor). The pid-level dedup stays as the output-uniqueness second layer (distinct cwd spellings may collapse to one project); ordering semantics unchanged (first-occurrence newest-first); bounded ledger read unchanged. Gate: impl — cwd-dedup before derivation; unit — derivation-count observable == distinct cwds not ledger rows (duplicate-cwd ledger derives once), output/order parity vs the pre-fix shape on a mixed ledger. SEEDED SEPARATELY (not this REQ): gossip leg consults the ADR-0037 materialized index (kills git entirely + cross-round rederivation; staleness-semantics design change) + epoch-mint batching (26 atomic writes/round; durable-monotonic contract change).
2026-09-06T13:09:02.0180030Z - Required stages: impl, unit
2026-09-06T13:09:02.0180187Z 
2026-09-06T13:09:02.0180291Z ### REQ-RC-HONEST-SESSION-AUTHORITY
2026-09-06T13:09:02.0183645Z - Title: RC-RENDER-TRUTH W1 (ADR-0042 decision 1, hertz perri-contradiction RCA): normal `spt rc` consults the ADR-0041 single honest-session authority BEFORE the persisted-offline fast-fail — run the bounded SessionProbe::has_live_session_honest gate; honest session exists means attach via run_attach_session_confirmed regardless of persisted status; no honest session means the existing offline refusal stands; claimed session with dead client tree means refusal/reap, NEVER attach. Reuse SessionProbe — no new liveness heuristic. Kills the authority split where rc refused ('offline — nothing to attach to') while endpoint run --resume reattached to the same live session. Gate: impl — the pre-fast-fail probe + session-confirmed routing; unit — probe-true routes to session-confirmed attach, probe-false keeps the refusal, dead-tree claim refuses; int — the 3-row regression matrix: offline persisted row + honest live broker session => rc attaches; offline + no session => existing refusal; zombie/dead client tree => refusal, never attach; doc — ADR-0042.
2026-09-06T13:09:02.0186716Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0186881Z 
2026-09-06T13:09:02.0186982Z ### REQ-RESUME-UNBOUND-STAMP
2026-09-06T13:09:02.0190480Z - Title: RC-RENDER-TRUTH W1 (ADR-0042 decision 2, operator-spotted second root seam): resume launch transitions an existing offline perch to UNBOUND before/with the broker spawn — UNBOUND semantics are fresh/resume-invariant (broker session exists + harness not bound = UNBOUND); generation/session-safe ROLLBACK to offline on spawn failure or session death; bind owns UNBOUND->ONLINE. Supersedes the rc.rs 'resume gets no UNBOUND stamp — accepted' boundary note (the pre-bind window can be PERMANENT: stuck native resume, SessionStart never fires — field-proven; truthful UNBOUND is the operator-recovery surface that let `spt rc` reach the wedged TUI). Writer-truth complement to REQ-RC-HONEST-SESSION-AUTHORITY — both land, neither substitutes. Gate: impl — UNBOUND stamp at resume spawn + rollback + bind transition; unit — stamp fires on existing-offline perch resume, rollback on spawn-fail restores offline, generation guard refuses a stale rollback over a newer bind; int — resumed-but-never-bound endpoint reads UNBOUND (not offline) and `spt rc` attaches to its live session; doc — ADR-0042.
2026-09-06T13:09:02.0193557Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0193723Z 
2026-09-06T13:09:02.0193829Z ### REQ-RC-HARNESS-ONLY-REFUSAL
2026-09-06T13:09:02.0196618Z - Title: RC-RENDER-TRUTH W1 (ADR-0042 decision 3, hertz elevated-endpoints RCA core leg 1): direct `spt rc <id>` preflights the already-known harness_only/controllable=Some(false) fact (local + gossiped rows both carry it) and refuses BEFORE any attach/stream with truthful copy naming the actual state ('online but harness-hosted; spt does not own its terminal') — never the stale-row guess ('it may have ended, or the registry row is stale'). Reproduced deterministically 2x live on an ONLINE - HARNESS ONLY endpoint. RC support for harness-hosted endpoints is NOT claimed until the elevated PTY-host satellite exists (standing ruling). Gate: impl — pre-stream preflight + truthful copy; unit — harness-only row refuses pre-dial with the new copy, non-harness rows unaffected; int — live remote Instance{harness_only:true} => direct rc refuses pre-stream, names harness-hosted, never says stale; doc — ADR-0042.
2026-09-06T13:09:02.0199232Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0199399Z 
2026-09-06T13:09:02.0199508Z ### REQ-RC-QUALIFIED-TARGET-CANONICAL
2026-09-06T13:09:02.0202462Z - Title: RC-RENDER-TRUTH W1 (ADR-0042 decision 4, hertz elevated-endpoints RCA core leg 2, doyle seam-confirmed rc.rs establish_attach): the resolver's canonical BARE endpoint id is carried separately from the user-facing qualified target — AttachRequest.endpoint_id is always the bare id (today rc passes the ORIGINAL qualified string; the target's resolve_local_session compares verbatim vs the bare HostedSession.endpoint, so `spt rc id@node`/`subnet:id` dials the RIGHT node then gets a false no-live-session refusal). N-1-additive: bare-form callers are unchanged. Gate: impl — canonical-id carry through establish_attach; unit — Address::parse qualified forms yield bare wire id, user-facing copy keeps the qualified spelling; int — bare + id@node + subnet:id ALL attach against a remote broker-hosted target, wire always carries the canonical bare id; doc — ADR-0042.
2026-09-06T13:09:02.0204860Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0205029Z 
2026-09-06T13:09:02.0205133Z ### REQ-HAZARD-RC-ATTACH-TRUTH
2026-09-06T13:09:02.0207245Z - Title: RC-RENDER-TRUTH W1 (KNOWN-HAZARDS 7.46 — umbrella conformance seam for ADR-0042): an rc surface answers from live session authority, never a stale persisted projection; a resuming perch is UNBOUND, not offline. Regression matrix from the hertz RCAs + operator field recovery: offline-row-over-honest-session attaches; offline-no-session refuses; zombie refuses/reaps never attaches; resume-never-bound reads UNBOUND and is attachable; harness-only refuses truthfully pre-stream; qualified targets attach with bare wire id. HEAVY nextest group at birth for any leg spawning a daemon tree (standing CI lint). Gate: int — the matrix; doc — KNOWN-HAZARDS 7.46.
2026-09-06T13:09:02.0209239Z - Required stages: doc, int
2026-09-06T13:09:02.0209391Z 
2026-09-06T13:09:02.0209502Z ### REQ-BROKER-OUTPUT-BEFORE-EXIT
2026-09-06T13:09:02.0212287Z - Title: RC-RENDER-TRUTH W3 (ADR-0043 decision 1, hertz stale-glyphs RCA leg 1 P0): the PTY drain/output writer is the SOLE FIFO sequencer for terminal Output + Exit per attach sink — Exit is enqueued BEHIND all prior output (drain EOF/completion first, then Exit); the exit waiter never direct-writes KIND_EXIT around the queued output path (a mutex serializes bytes, not producer order). Kills the stranded-final-frame race (final EL/SGR-reset/cursor-show/?1049l lost when Exit overtakes Output — already admitted and compensated in the broker test suite, never fixed in production rc). Gate: impl — single sequencer, exit-behind-output enqueue; unit — ordering invariant on the writer queue (exit never precedes queued output for a sink); int — short-lived child emits 'XXXX ESC[2K ESC[?25h ESC[?1049l' then exits => that exact Output precedes Exit through the PRODUCTION broker->attach->rc path; doc — ADR-0043.
2026-09-06T13:09:02.0214712Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0214883Z 
2026-09-06T13:09:02.0214987Z ### REQ-BROKER-EXIT-EVERY-REAP
2026-09-06T13:09:02.0222314Z - Title: PORTER (releases#201, todlando mechanism pass 2026-08-21): the broker exit waiter emits a terminal KIND_EXIT for EVERY reap it performs, INCLUDING the reap whose PtySession::wait() yields Err. This requirement states EXISTENCE; REQ-BROKER-OUTPUT-BEFORE-EXIT states ORDERING — the two are separate seams over the same frame, and any Exit this requirement adds is bound by that one's exit-behind-output enqueue discipline. TODAY broker.rs's `if let Ok(status) = status` gates the WHOLE Exit emission block, and only that block: the row removal, the best-effort kill, the drain bytes_forwarded quiesce and the reap-time unlatch (stamp_reaped, REQ-HAZARD-CONTROL-STAMP-LIFETIME) all sit OUTSIDE it and all still run on the Err arm (measured at b88fab2a: block spans broker.rs 6430-6457, stamp_reaped at 6467). So the session is genuinely, completely gone from the table AND the perch is unlatched, while NO Exit frame ever reaches the controller or viewer sinks — the attached rc has no terminal event to act on and burns its full 30s reconnect window over a session that can never come back. The Err arm is reachable in the field, not only under injection: PtySession::wait (spt-term/src/pty.rs:203) is `self.child.lock().unwrap().wait().map_err(SurfaceError::Io)`, so ANY Io error on the child handle takes it — the kill-vs-natural reap race included, where our own kill() and the child's natural exit contend for the same handle. AN ABSENT EXIT CODE IS NOT A ZERO AND IT IS NOT A NO-RECORD (the absent-is-not-zero discipline, releases#197): ExitEvent.code is ALREADY Option<i32>, so the Err arm can name the death with the code ABSENT rather than fabricating a 0 or suppressing the frame — a suppressed frame is indistinguishable to the viewer from a session still running. Gate: impl — Exit enqueued on BOTH arms of the wait outcome, code absent-but-named on Err, still behind the drain quiesce; unit — the event built for an Err reap carries code: None and is not skipped; int — a NARROW fault valve makes wait() yield Err while row removal, drain quiesce and stamp_reaped ALL still run (the existing SPT_TEST_EXIT_WAITER_SUPPRESSED is precedent for the shape but suppresses the ENTIRE waiter, so it does NOT reproduce this face) => the attached controller and viewer OBSERVE an Exit frame, with the valve-off control arm proving the same panel observes the healthy Exit (non-vacuity: a panel no assertion depends on is untested by construction); doc — KNOWN-HAZARDS.
2026-09-06T13:09:02.0235197Z - Required stages: impl, int
2026-09-06T13:09:02.0235373Z 
2026-09-06T13:09:02.0235469Z ### REQ-RC-VT-TEARDOWN
2026-09-06T13:09:02.0238691Z - Title: RC-RENDER-TRUTH W3 (ADR-0043 decision 2, hertz stale-glyphs RCA leg 2 P0): rc display teardown is a display RAII guard SEPARATE from the OS input/raw-mode guard, unconditional and idempotent on EVERY exit path including errors and unwind — best-effort SGR reset + full scroll-region reset + cursor show + leave alternate screen + clear/home, emitted while VT output processing is still enabled, THEN restore the prior console output mode, THEN parting prose (today RawGuard::drop restores raw/mouse/console-mode only; detach, child exit, displacement, first-event stall, fatal error, and the 30s reconnect give-up all can leave the operator terminal dirty; the reconnect banner clears+homes then give-up prints at the centered cursor). Gate: impl — split display guard + every-path coverage; unit — guard emits the cleanup postlude exactly once, idempotent on double-drop; int — dirty sink (?1049h ?25l SGR31) x every PumpEnd/error class => cleanup postlude precedes the final prose; doc — ADR-0043.
2026-09-06T13:09:02.0241492Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0241672Z 
2026-09-06T13:09:02.0241773Z ### REQ-PICKER-PURGE-STRUCTURED
2026-09-06T13:09:02.0245046Z - Title: RC-RENDER-TRUTH W3 (ADR-0043 decision 3, hertz stale-glyphs RCA leg 3 P0): core verbs invoked from inside an active TUI return STRUCTURED outcomes and write NOTHING to the terminal — the picker purge path calls a structured-outcome purge core (no stdout/stderr under the live alternate screen) and remains the sole renderer via model.flash (today cmd_endpoint_purge writes diagnostics/PURGED to stderr while ratatui owns the alt screen, mutating the physical screen behind the previous-Buffer diff baseline => later draws skip 'already blank' cells and stderr glyph fragments persist — the x-purge symptom). Baseline-desync regression REQUIRES a stateful/recording backend (pure TestBackend view snapshots cannot catch it). Gate: impl — structured purge outcome + silent-under-TUI routing; unit — purge core emits no terminal bytes in structured mode, picker converts outcomes to flash; int — recording backend: draw ConfirmPurge, inject an external display mutation, transition back => next frame reconstructs the COMPLETE target screen; doc — ADR-0043.
2026-09-06T13:09:02.0248171Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0248341Z 
2026-09-06T13:09:02.0248450Z ### REQ-SCREENGRID-REPAINT-MODE-REPLAY
2026-09-06T13:09:02.0250828Z - Title: RC-RENDER-TRUTH W3 (ADR-0043 decision 4, hertz stale-glyphs RCA leg 4 P1): ScreenGrid cold repaint replays EVERY tracked render-affecting mode — DECSTBM scroll margins at minimum — before final cursor placement (today render_repaint omits tracked margins, so client and server grids interpret subsequent raw scrolling against different regions => stale/moved rows after reattach/resize; the trailing-blank omission after ED2 is semantically correct and NOT the bug). Stateful emulator contract: dirty screen + synthesized repaint + next raw frame == server grid. Gate: impl — tracked-mode replay in render_repaint; unit — repaint emits tracked DECSTBM, emulator contract holds for scroll-after-repaint; doc — ADR-0043.
2026-09-06T13:09:02.0252890Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.0253058Z 
2026-09-06T13:09:02.0253167Z ### REQ-HAZARD-RENDER-LIFECYCLE
2026-09-06T13:09:02.0255230Z - Title: RC-RENDER-TRUTH W3 (KNOWN-HAZARDS 7.47 — umbrella conformance seam for ADR-0043): the physical terminal is never mutated or terminated outside its renderer's ordered state model. Regression matrix from the hertz RCA: output-before-exit through the production path; unconditional display teardown across every rc exit class; TUI baseline reconstructs whole after out-of-band mutation (recording backend); repaint replays tracked modes. Deferred P2 seeds recorded, NOT this milestone: Exit{after_seq} watermark defense; per-client semantic baseline (only if spt ever transforms live frames). Gate: int — the matrix; doc — KNOWN-HAZARDS 7.47.
2026-09-06T13:09:02.0257075Z - Required stages: doc, int
2026-09-06T13:09:02.0257219Z 
2026-09-06T13:09:02.0257328Z ### REQ-CONTROLLER-LEASE-IDENTITY
2026-09-06T13:09:02.0265804Z - Title: RC-RENDER-TRUTH W2 (ADR-0044 decisions 1+2, hertz same-machine --take split-brain RCA P0-A/B, doyle seam-verified broker.rs resolve_subscribe 1296-1318 + 1334-1347): each rc invocation/attach stream mints a UNIQUE controller lease id carried through SubscribeReq, the controller slot, and Input/Resize; node identity stays separate as attribution/access policy only. The ONLY silent successor/replay case is same-lease + equal-or-newer generation (ADR-0038 fix-6 dispatcher-recovery contract preserved exactly); same node + different lease = DISTINCT controller. Explicit Take on a distinct incumbent lease ALWAYS revokes loudly AND authoritatively — atomically revoke/fence the old lease and FORCE its attach stream closed, then install the taker, regardless of whether by-node strings match; the Displaced notice is best-effort (today old.tx.try_send at broker.rs:1342-1345 can DROP the notice on a Full queue while become_controller still replaces — the revoke/close must land even when notice enqueue fails; the closed stream is itself the terminal signal rc's PumpEnd::Displaced/EOF path handles). Today same_identity keys on controller_by()==by alone, intent never consulted; two same-machine windows collapse to one identity and --take silently swaps the sink leaving the incumbent input-capable but blind. Acceptable identity seam: (by, nonzero attach generation) as the logical lease. GENERATION LADDER (doyle ruling 2026-07-18, resolves fix-6/Busy conflict at T6): within the SAME by — equal gen = silent successor (dispatcher recovery, unchanged); STRICTLY NEWER gen (Control or Take) = loud+fenced supersession (full W2 revoke — without it a live replacement viewport bounces Busy off a stale-replay worker serving a zombie stream forever = post-restart lockout, fix-6 order-independence regression); OLDER gen = refused Busy (unchanged). Across DIFFERENT by: Control = Busy, Take = loud revoke. Deliberate UX: a second same-node window's plain rc loudly displaces the first (pre-W2 did the same replacement SILENTLY with the loser blind-but-interactive; loud+fenced strictly better; --view = coexistence). redispatch_stall T6 must stay green UNMODIFIED. N-1: lease field additive; absent lease (older client) degrades to legacy node-identity semantics for that subscriber. Gate: impl — lease mint + carry + loud distinct-lease Take; unit — same-lease equal/newer gen stays silent, same-node different-lease Take displaces loudly, older-gen replay still refused busy; int — hertz regression steps 1-4 + 7-8 (A gets Displaced{by} + terminal stream completion, output reaches B only, metadata identifies B, one controller slot; separate equal-lease replay test proves dispatcher recovery stays silent); doc — ADR-0044.
2026-09-06T13:09:02.0273206Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0273376Z 
2026-09-06T13:09:02.0273478Z ### REQ-INPUT-CONTROLLER-FENCE
2026-09-06T13:09:02.0278186Z - Title: RC-RENDER-TRUTH W2 (ADR-0044 decision 3, hertz same-machine --take split-brain RCA P0-C + scope clarification, doyle seam-verified broker.rs dispatch_input 3920-3935 session-addressed unfenced): broker-enforced input fencing SCOPED TO RC-ORIGIN INPUT — RC Input/Resize bind to the ACTIVE controller lease (or originating broker connection as the N-1 surrogate); commands from a displaced/stale lease are rejected/dropped after replacement. Do NOT globally gate generic KIND_INPUT: shell/system injection legitimately sends InputReq from non-controller connections (Minter::Shell, shellchan seam) — fence keys on an additive controller-ownership token validated only for token-bearing/Minter::Rc requests, or a dedicated guarded RC-input verb; token optional/default-none preserves generic injection exactly. REQUIRED DEFENSE, not optional hardening: this is what makes the at-most-one-input-capable-controller invariant TRUE even when the Displaced notification is delayed or lost (today the displaced window keeps typing into the PTY indefinitely — the field split-brain). Gate: impl — token/verb + lease-bound validation on the RC input path; unit — stale-lease RC input rejected post-replacement, active-lease applies, tokenless generic injection (shell) UNCHANGED, absent-lease N-1 falls back to connection fencing; int — hertz regression steps 5-6 (post-takeover Input+Resize from A mutate NOTHING, from B both apply) + shell injection still lands mid-controlled-session; doc — ADR-0044.
2026-09-06T13:09:02.0282418Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0282578Z 
2026-09-06T13:09:02.0282726Z ### REQ-HAZARD-CONTROLLER-LEASE
2026-09-06T13:09:02.0285680Z - Title: RC-RENDER-TRUTH W2 (KNOWN-HAZARDS 7.48 — umbrella conformance seam for ADR-0044): at most one input-capable controller lease per PTY session; takeover revokes atomically and loudly; input is fenced to the active lease; node identity is never a lease. The full hertz 8-step deterministic two-loopback-client broker regression rides verbatim: A subscribes Control from node N and controls; B subscribes Take from the SAME node with a different lease; A receives Displaced{by:N} then terminal stream completion (rc exits via existing PumpEnd::Displaced); output post-takeover reaches B not A; A's Input+Resize post-takeover mutate nothing; B's both apply; controlled/driven_by metadata identifies B with exactly one controller slot; a separate equal-lease/equal-generation replay test proves genuine dispatcher recovery remains silent and never self-displaces. Gate: int — the matrix; doc — KNOWN-HAZARDS 7.48.
2026-09-06T13:09:02.0288145Z - Required stages: doc, int
2026-09-06T13:09:02.0288290Z 
2026-09-06T13:09:02.0288407Z ### REQ-DRIVEN-BY-OWN-NODE-NORMALIZE
2026-09-06T13:09:02.0298787Z - Title: RC-RENDER-TRUTH v0.38.1 fast-follow leg 1 — RESHAPED by operator/hertz correction + CONTEXT.md:382-389 grounding (doyle ruling v3, 2026-07-19; the original own-node NORMALIZATION is WITHDRAWN): plain `spt rc` on a CONTROLLED endpoint is refused-with-guidance BY DESIGN (CONTEXT:386 — never silent-displace; --take is the opt-in kick), and `driven_by` = the CONTROLLING NODE including the own node (CONTEXT:386 single-writer datum — the stamp comment's remote-only claim was a legacy-path artifact, NOT the model). v0.38.0 shipped behavior is CORRECT: the own-hex latch + client guidance blocking a second same-node plain rc IS the documented refusal, and the W2 generation ladder stays reachable exactly where it belongs — the recovery seams (rc reconnect re-drive + dispatcher re-serve bypass the client gate; the hertz field FAIL leg is reclassified NOT-A-DEFECT). Remaining work = truth/cosmetics across ALL own-node display surfaces (doyle gate finding 2026-07-19 — the own-hex latch is truthful but only rc.rs first learned to HUMANIZE it; the picker pin + endpoint-info attached_node still read the own-hex stamp as a foreign remote driver and print raw hex): (a) rc.rs guidance copy names the actual state — controlled from another window on this machine — via a pure driver_phrase keyed on the SHARED own-node predicate (roster::is_own_node_hex); (b) picker control_line (view.rs) reads an own-node driver as controlled locally (driven_by_is_self computed at data-build where the raw hex is available, driven_by STAYS populated so View+Kick is still offered); (c) endpoint-info attached_node (reporting.rs derive_attached_node) self-attributes an own-node driver to THIS node, never a foreign null-label hex; (d) stamp_driven_by + set_driven_by/set_controlled comments rewritten to the CONTEXT:386 meaning (controller node, own included) + KH 7.15/secondary remote-only model-claim comments reconciled (selfheal/unlatch semantics unchanged) — STAGE-NOTE CORRECTION (todlando 2026-08-19, doyle-ruled): this item shipped only its FIRST half. The WRITER-side comments were rewritten (broker.rs stamp_driven_by, info.rs set_driven_by, info.rs set_controlled all carry the ruling and are the anchors). The `secondary remote-only model-claim comments reconciled` half was NOT executed: the InfoJson FIELD docs for driven_by/controlled, picker/model.rs's controlled field doc (which contradicted driven_by_is_self twelve lines above it in the same struct), and the REQ/triage records that restate the falsified model all still carried remote-only. Executed by the sweep commit that carries this correction. No prod behavior site ever encoded remote-only — display_status keys on driven_by || controlled, correct under both models — which is why the stale premise never shipped a bug; (e) plain-Control-on-controlled refusal + --view/--take bypass pinned for BOTH remote and own-node drivers. NO broker behavior change; ladder untouched; pure display (no int, same as the ruling-v3 dropped int). Gate: impl — rc/picker/endpoint-info humanization + shared predicate + comment/docs; unit — own-node latch stamps truthfully (broker), guidance fires for own+remote with the right copy (rc), driver_phrase names own-node plainly (rc), control_line reads own-node as controlled locally (view), attached_node self-attributes own-node (reporting), --view/--take bypass; doc — KH 7.15 + ADR-0044 consequence-note alignment.
2026-09-06T13:09:02.0307881Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.0308054Z 
2026-09-06T13:09:02.0308158Z ### REQ-RC-SINGLE-PUMP-BRAIN
2026-09-06T13:09:02.0312097Z - Title: RC-RENDER-TRUTH v0.38.1 fast-follow leg 2 (hertz v0.38.0 field repro 2, hertz RCA confirmed + doyle-accepted): plain `spt rc` constructs EXACTLY ONE pump Brain — the W1 truth probe (SessionProbe::connect, rc.rs ~1388/981-987, KIND_SESSIONS then drop) and establish_attach (~1464/1632) each build a real pump Brain today = two transient IPC reader threads/conns + a doubled user-visible 'PUMP_IPC_READER: spawned' banner per invocation (brain.rs:254 emits once per BrainConn::split_with_reader via cold_start_pump — the log site is NOT duplicated). FIX (hertz seam, ratified): carry the SessionProbe's Brain INTO establish_attach and re-query sessions on that same conn for freshness — do NOT suppress the log line and do NOT switch to Whole (the banner is truthful; the double construction is the defect). Qualified/session-confirmed paths (which skip the probe) and the reconnect loop (one fresh pump per attempt, correct) unchanged. Gate: impl — probe-Brain carry + same-conn freshness re-query; unit — probe-then-establish reuses the conn (construction-count observable); int — rc_attach_truth offline_row_over_live_session_attaches extended: capture stderr, assert PUMP_IPC_READER spawned count == 1 PLUS existing behavior assertions; doc — none (internal seam).
2026-09-06T13:09:02.0315641Z - Required stages: impl, unit, int
2026-09-06T13:09:02.0315837Z 
2026-09-06T13:09:02.0315951Z ### REQ-HOSTING-AUTHORITY-CONTROLLABLE
2026-09-06T13:09:02.0323610Z - Title: RC-RENDER-TRUTH v0.38.1 fast-follow leg 3 (hertz todlando immortal-hybrid trace, doyle fork ruling 2026-07-18 = controllable-authority): ONE hosting authority — persisted `state` stays the durable endpoint TYPE (REQ-EP-6 open type system; establish_perch's prior-type preserve at startup.rs:346-350 is INTENTIONAL and stays), `controllable==Some(true)` is the source-definitive broker-PTY authority. FIELD ROOT (C1 coverage gap, not new family): an spt-hosted bind over a prior ready_agent perch preserves state=ready_agent while stamping controllable=true + online -> livehost restart_resume_gate (508-534) Skip's state!=live_agent so the orphan never resumes, and reconcile's C1 dead-pid hybrid heal predicate was scoped controllable=false so controllable=true escapes -> immortal dead-PID ready_agent hybrid, latch-driven Active projection (todlando field state; rc's no-session refusal was TRUTHFUL). FIX (hertz refinement, ratified): remove the state rejection from restart_resume_gate; reconcile routes only non-live_agent && controllable!=Some(true) through the PID-model hybrid heal, while controllable==Some(true) rows fall through BROKER-SESSION truth (orphan with ledger/adapter material => Resume; no session + dead pid => terminal offline via the W2 atomic normalize). Psyche hosting stays separately state-gated (live_agent only). endpoint_survival tables BOTH live_agent and ready_agent broker-owned rows -- both resume their PTY at daemon start, ready stays no-Psyche (hertz definitive-trace addendum). restart_resume_gate keys on online+controllable+session/relay/custody belts, no state arg. cmd_bind's online gate reads/verifies the PERSISTED state it just wrote, never only the requested arg (parity with cmd_listen's W2 creator gate). Gate: impl -- gate/reconcile routing + bind online-gate persisted-read; unit -- routing table (non-live+controllable!=true -> PID-model; ready+true -> session-truth; live_agent unchanged); int -- hertz matrix verbatim: (i) ready_agent+controllable=true+online ORPHAN (dead harness, ledger+material) => restart RESUMES; (ii) ready+true+online with dead pid/NO session => reconcile terminally offlines + Active projection removed; (iii) legitimate ready listener (controllable!=true, live pid) => stays messaging-online on the PID model, never treated as PTY-attachable; (iv) spt-hosted bind over prior ready_agent => controllable=true+online+restart-resume works with preserved type; doc -- ADR-0041 amendment note (authority split: type vs hosting).
2026-09-06T13:09:02.0330338Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0330501Z 
2026-09-06T13:09:02.0330615Z ### REQ-ENDPOINT-TEARDOWN-AUTHORITY
2026-09-06T13:09:02.0342677Z - Title: TEARDOWN-AUTHORITY W1 (ADR-0045; two hertz field RCAs 2026-07-19, doyle code-verified + ruled): ONE shared topology-aware broker-teardown primitive behind BOTH `endpoint shutdown` and `endpoint stop`. Today both verbs stamp state they never cause: cmd_shutdown (cli.rs:3979) = remove ready marker + cmd_rest(Suspend) — the Suspend edge (resting.rs daemon_rest_event_with_liveness -> apply_event -> cascade_shells_on_edge -> advertise) fires echo + shell cascade + advertise and NEVER touches a session, even though the verb already probed broker-session truth (cli.rs:3797-3804 has_live_session_honest) to force from=alive; cmd_stop (cli.rs:7071) = marker + unregister_address + terminal_normalize + advertise, whose own comment calls it a DEFINITIVE death observation it fabricates. Field: broker retains the whole subtree (adapter -> node -> harness -> nested resumed harness + MCP children; the surviving `spt api listen` is a DESCENDANT, so a direct-child kill misses it). PRIMITIVE (ordered, ADR-0045 decisions 1/6/7/9): resolve broker SessionInfo -> dedicated sid/endpoint-keyed broker kill claiming NO controller (NOT Brain::attach()+kill_session(), brain.rs:622 require_session() = controller theft under the ADR-0044 lease ladder; idempotent — unknown session is benign success) -> reap the whole DESCENDANT subtree (reuse spt_store::proc.rs kill-root+process_descendants, do not mint a second tree-walk) -> await broker-row removal under a bound -> ONLY THEN stamp + unregister + advertise. Stamp-after-reap is load-bearing: field-observed post-stop info.json read status=offline BUT rest_state=active + controlled=true because the SURVIVING host RE-BOUND after the CAS-less terminal_normalize(path, None) — no hardening of the write fixes this, only the reap makes the stamp true (shutdown passes Some(sid) for the concurrent-newer-bind CAS; stop's unconditional posture stays, it is the operator's own definitive command). TOPOLOGY SPLIT (decision 2): gated controllable==Some(true) (ADR-0041 hosting authority — the SAME predicate cmd_bind's online-earn and the livehost reconcile use); harness-hosted/external keeps marker+address+status-only behavior (CONTEXT.md:39 — core spawned nothing, holds nothing) and the misleading cmd_shutdown doc comment claiming marker-removal stops the listener is corrected to name its topology. VERB DIFFERENCES ARE ONLY ceremony + resulting intent (decision 3): shutdown = echo commune + shell cascade FIRST then teardown, result rest_state=suspended (wrap the EXISTING rest edge — apply_event already echoes before the flip per KH 3.3; do NOT reimplement the ordering); stop = NO ceremony, result terminal_normalize. TIMEOUT (decision 7): do NOT stamp cold over a survivor — exit non-zero, and because stop is the last rung with no in-band escalation behind it its failure line names the surviving ROOT PID + the scoped-kill remedy. reconcile_hosted_liveness stays the partial-failure catch-up net, not duplicated. Precedent: REQ-HAZARD-DAEMON-STOP-REAP already ruled this reap principle at DAEMON scope; this is the ENDPOINT scope it was never extended to. Gate: doc — ADR-0045 + CONTEXT.md amendments (190 record-vs-process axis, 640 verb semantics, 647 stop echo exception) + KNOWN-HAZARDS entry; impl — the shared primitive + both verb call sites + topology gate + corrected comments; unit — topology routing table (controllable=true -> teardown, harness-hosted -> marker/address/status only), verb-difference table (ceremony + resulting intent), timeout posture (no cold stamp on survivor, failure line carries root pid), REWRITE shutdown_soft_stops_and_suspends (it asserts marker+intent only = it asserts the bug); int — real broker-hosted endpoint: after each verb, no broker session row + process subtree gone INCLUDING the descendant `spt api listen` + perch ready=false/alive=false/address unregistered/unbound=false + effective and advertised state correct + (shutdown) echo/cascade ran BEFORE the kill + wake/resume works from the recorded sid under the recorded adapter.
2026-09-06T13:09:02.0354734Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0354924Z 
2026-09-06T13:09:02.0355039Z ### REQ-TEARDOWN-UNCOOPERATIVE-HOST
2026-09-06T13:09:02.0357772Z - Title: TEARDOWN-AUTHORITY W1 (ADR-0045 decision 4): the teardown kill NEVER depends on harness cooperation — no graceful-input path, no waiting on PTY EOF, no ask-it-to-exit-first step. A WEDGED host is the design case, not the edge case: it is the situation that produced the ADR (doyle's own endpoint — read-only rc replayed the retained PTY and `rc --take` acquired control, but a direct prompt produced no output for 30+s because the retained harness subtree itself was nonresponsive while the broker correctly preserved and replayed its last PTY state). Any cooperation-dependent step re-imports the exact hang the verb exists to break. Gate: impl — the kill path proves no dependency on session responsiveness; int — a deliberately nonresponsive/wedged hosted harness is still reaped within the bound (broker row gone + subtree gone), asserted against a real broker-hosted session.
2026-09-06T13:09:02.0360449Z - Required stages: impl, int
2026-09-06T13:09:02.0360601Z 
2026-09-06T13:09:02.0360701Z ### REQ-HAZARD-TEARDOWN-DEADEND
2026-09-06T13:09:02.0365512Z - Title: TEARDOWN-AUTHORITY W1 HAZARD (ADR-0045 decision 8; hertz RCA 2 — hit doyle's OWN live production endpoint, recoverable only by out-of-band scoped kill): `endpoint stop` followed by `endpoint start <same>` MUST succeed (a fresh session or a resume) and must NEVER answer ENDPOINT_CREATE_CONFLICT about a session that stop claimed to end. TRAP SHAPE (two individually-CORRECT behaviors composing into a lifecycle DEAD END with no in-band exit): (1) stop leaves the broker session alive (REQ-ENDPOINT-TEARDOWN-AUTHORITY), and (2) `endpoint run` correctly REFUSES ENDPOINT_CREATE_CONFLICT rather than silently reattaching (v0.37.0 no-silent-reattach rule, deliberately chosen). The survivor is therefore simultaneously what stop claims to have killed AND what run refuses to work around; with a wedged harness every in-band verb is exhausted (stop lies, run refuses, rc replays a dead PTY, rc --take controls a process that never answers). Neither behavior is individually wrong — the COMPOSITION is the hazard, so the regression must assert the composition, not either verb alone. This single assertion is the whole user-visible point of W1. Gate: impl — covered by the shared primitive; int — start a real broker host, hard-stop it, assert subtree + broker row gone, THEN `endpoint start <same>` SUCCEEDS (never CREATE_CONFLICT) and `endpoint run --resume <recorded sid>` creates a NEW PTY rather than rc-ing the old one; PLUS the durability leg — info stays offline + terminally-normalized across a bounded settle window (proves no re-bind by a survivor, the field rest_state=active finding made into an assertion).
2026-09-06T13:09:02.0369983Z - Required stages: impl, int
2026-09-06T13:09:02.0370132Z 
2026-09-06T13:09:02.0370227Z ### REQ-SCREENGRID-WIDTH
2026-09-06T13:09:02.0375296Z - Title: TEARDOWN-AUTHORITY W3 (hertz field RCA 2026-07-19, doyle-confirmed at source): ScreenGrid models every character as ONE display column, so wide characters (CJK, emoji, and other 2-column glyphs) shift subsequent text left and leave stale scraps at the right margin — field repro on the Claude settings UI (left-shifted rows + To/Wh/Es left-margin scraps). SOURCE: spt-term/src/screen.rs Cell { ch: char, pen } (~127) carries NO width datum, and put_char (~327-343) unconditionally advances col += 1, with pending_wrap likewise advancing a single display cell. FIX: give the grid a real display-width model — a wide glyph occupies its leading cell plus a continuation cell that renders nothing and is never independently addressable; cursor motion, wrap, erase, and scroll all reckon in DISPLAY columns. WIDTH POLICY (hertz-proposed, doyle-ratified — pinned so the renderer and the emulator can never disagree): share ratatui's pinned unicode-width 0.2.0 as a WORKSPACE dependency, ambiguous-width = 1, no CJK-context override. A second width authority in the tree is the defect this policy exists to prevent. Gate: doc — the width policy stated where the grid is documented; impl — width-aware Cell/put_char/wrap + display-column reckoning across cursor/erase/scroll; unit — wide-glyph advance + continuation-cell invariants, wrap at the right margin with a wide glyph that cannot fit, erase/overwrite of a continuation cell clears the whole glyph, combining/zero-width marks do not advance; int — regression oracle against an INDEPENDENT emulator's rendering of the same byte stream (the field repro shape: wide glyphs followed by EL and CUP row-addressed redraws must leave no left-shift and no margin scraps).
2026-09-06T13:09:02.0379903Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0380072Z 
2026-09-06T13:09:02.0380160Z ### REQ-DSR-SINGLE-CPR
2026-09-06T13:09:02.0381932Z - Title: TEARDOWN-AUTHORITY W3 rider (hertz RCA 2026-07-19): a Device Status Report query yields EXACTLY ONE Cursor Position Report. Rides W3 because it is the same emulator-conformance surface as the width work and the same field capture surfaced it; kept a separate REQ so its evidence is not buried inside the width tags. Gate: impl — one CPR per DSR on the emulator reply path; unit — a DSR in the input stream produces a single well-formed CPR carrying the DISPLAY-column cursor position (i.e. consistent with REQ-SCREENGRID-WIDTH reckoning, not the raw char count).
2026-09-06T13:09:02.0383688Z - Required stages: impl, unit
2026-09-06T13:09:02.0383830Z 
2026-09-06T13:09:02.0383936Z ### REQ-RC-RESIZE-GEOMETRY-EPOCH
2026-09-06T13:09:02.0404583Z - Title: A resize transition is ATOMIC from ScreenGrid's perspective: every byte is parsed at the geometry it was EMITTED under. (hertz v0.39.0 field RCA 2026-07-21, doyle re-grounded at source the same hour; P0 — this falsifies a SHIPPED v0.39.0 release claim, so it is a regression, not new scope.) SYMPTOM: right-margin fragments and left-shifted rows on a node where CLI and broker are BOTH 0.39.0 — i.e. exactly the defect the v0.39.0 notes claim fixed. SOURCE (statically provable, no repro box required): broker.rs dispatch_resize calls session.resize(SurfaceSize::new(rows, cols)) (~4557-4559) and only THEN recover_log(&log).set_size_and_notify(rows, cols) (~4560), which reaches self.grid.resize(rows, cols) (~1492) — TWO SEPARATE recover_log acquisitions, so the reader thread's append/parse interleaves between them BY CONSTRUCTION. On ConPTY the resize ITSELF emits a full repaint asynchronously; the reader parses that repaint into the grid at the OLD geometry; the subsequent top-left-preserving grid.resize then faithfully preserves an ALREADY-MISWRAPPED model; the cold attach repaint emits the shift. WHY THE EXISTING TESTS PASS: the spt-term width oracle (REQ-SCREENGRID-WIDTH's int stage) is green 5/5 while the field is red — it never crosses the ConPTY-emission -> grid-geometry -> attach-repaint seam, and the surface mock structurally cannot. The W3 width model is therefore NOT falsified; it is correct work running under a broken geometry transition. FIX — and the record must carry WHY the obvious fix is wrong: merely REORDERING (set grid geometry before invoking the ConPTY resize) is INSUFFICIENT. It is safe on a GROW and unsafe on a SHRINK, because bytes already in flight at the old WIDER geometry are then parsed at the new narrow width and wrap early. Neither pure ordering is correct because the defect is not the ORDER — it is that the grid has no notion of 'these bytes were emitted under the old geometry'. Required shape: quiesce/gate the drain across the ordered surface+grid transition AND carry an explicit geometry EPOCH so parse always reckons at the emitting geometry, with rollback/refetch of the grid geometry if the surface resize fails. Gate: doc — the epoch/barrier contract stated where the resize path is documented, including the shrink counter-example so the insufficient reorder is not re-proposed; impl — barrier + geometry epoch + failure rollback in the dispatch_resize/set_size_and_notify path; unit — a resize whose repaint bytes arrive mid-transition is parsed at the emitting geometry (both grow AND shrink), and a failed surface resize leaves the grid geometry unchanged; int — Windows REAL ConPTY (not the surface mock): resize emits a dense frame, then the synthesized attach repaint is compared against an INDEPENDENT terminal authority, never ScreenGrid against itself. FIXTURE AMENDMENT (hertz field supplement 2026-07-21 second capture, doyle-accepted, carried by todlando at build): the dense frame is ordinary readable PROSE with REPEATED WORDS and punctuation, not sparse synthetic markers — the field captures are semantic corruption of rendered content (`ReQ2`, `ROADMAPnsayh'credertigls`, `Recommend:gdirect REST`, `thii reqw st`, `resdluuion`: clobber, merge, substitution and displacement of width-1 cells MID-ROW), and repeated words are what make a displaced fragment land somewhere that still looks plausible — the shape that let this read as cosmetic. The leg must fail on DELETION, INSERTION, SUBSTITUTION, MERGE or DISPLACEMENT of any width-1 cell, with NO normalization and NO presence-only assertions (full-row untrimmed string equality plus equal row counts satisfies this by construction). CONSTRAINT, binding: the fixture stays inside the region where the authority emulator and ConPTY AGREE — the screen at the resize instant carries no line reaching either margin and loses no row, so reflow-vs-truncate is a no-op on it, and the wrap-crossing prose is emitted AFTER the transition opens, at the new geometry. A fixture crossing the margin at the resize instant would red on emulator POLICY DIVERGENCE rather than on this defect; pinning the wrap policy between authority and ConPTY is separate work and must not be smuggled in here. The comparison is EVERY CELL, including cells expected to be BLANK — not row starts, right margins, or text presence. The dense frame must include erased interior spans and repeated spaces, and those cells must still be blank after the synthesized repaint. (hertz field supplement 2026-07-21, doyle-accepted: the operator confirmed a THIRD corruption shape on the same fully-0.39.0 node — stale characters surviving INSIDE nominal whitespace between words, e.g. `isnsettled`, `fulllyacaptured`. A mis-widthed repaint places glyphs into wrong interior cells; later differential output and EL operations address the CORRECT geometry and so do not necessarily clear them; the synthesized cold repaint then faithfully re-emits the contaminated interior. Accepted as further evidence for THIS requirement, not a new root, on a DISCRIMINATION rather than the prediction: the captured debris is pure ASCII, every glyph width-1, so no width-table or wide-half defect can produce it — and the erase paths are already width-normalized on both halves at screen.rs clear_cell_keep_pen ~416/~420, the span helper ~440-443, and delete/insert_chars ~617-620, with erase_across_a_wide_half_leaves_no_orphan ~1391 passing. Word-scale `isn`+`settled` collapsing across a space is the same shifted-by-N placement as the whole-row left shift, observed at word scale. OPEN DISCRIMINATOR: interior debris in a session where NO resize occurred at any point would mean a SECOND root that this fix will not clear.) BINDING CONSTRAINT on satisfying the blank-cell assertions: NO whitespace special-casing and NO extra clears — the barrier must restore ONE geometry authority for glyph placement AND erasure. A fix that passes by clearing harder satisfies the test and leaves the defect. KNOWN ACCEPTED RESIDUAL (doyle ruling 2026-07-21): a cold attach landing DURING a transition renders the PRE-TRANSITION screen — the held bytes are not parsed yet. Accepted because it is a strict improvement over the shipped behavior on the same input (pre-fix: miswrapped/corrupt; post-fix: stale but coherent). The window is bounded in TIME (settle + one surface call) AND — since REQ-RC-RESIZE-PRESENTATION-BARRIER (built 2026-07-21) — in VISIBILITY: the commit/abort-time sync frame delivers every attached sink, transition-era attachers included, the fresh repaint the moment the barrier tears down, quiet child or not (the successor REQ's commit-time sync frame IS the follow-up seed this residual carried, absorbed by construction). The attach itself still renders the pre-transition screen for the width of the window. REFUSED, so nobody builds it later without a fresh argument: making the attach repaint WAIT on an in-flight transition — that puts a user-facing attach behind the settle cap / serialize wait.
2026-09-06T13:09:02.0422624Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0422793Z 
2026-09-06T13:09:02.0422904Z ### REQ-RC-RESIZE-PRESENTATION-BARRIER
2026-09-06T13:09:02.0447521Z - Title: During a resize transition, LIVE DELIVERY is barriered along with the grid parse: no attached sink (controller or viewer) ever receives mixed-geometry raw bytes; commit substitutes ONE synthesized sync frame at the target geometry. (hertz post-fix field RCA 2026-07-21 on a fully-0.39.3 node, doyle-accepted with every cite verified at source; SUCCEEDS REQ-RC-RESIZE-GEOMETRY-EPOCH, which shipped correct but scoped to the wrong surface — broker.rs's own ResizeTransition doc states append 'still rings + fans out every chunk exactly as before' during a transition, and OutputLog::append gates ONLY grid.advance. A live attached terminal therefore receives old+new-geometry differentials across an already-resized viewport; the cold-attach repaint model is definitionally blind to that path, which is why the shipped gate was green while the field was red. REQUIREMENT-SCOPE defect: the gate held against documented design.) CONTRACT (ADR-0031 Amendment II): (1) append still assigns seq and rings every chunk, but controller handoff and viewer fan-out are SUPPRESSED during the transition — a skip, never a block or sleep (KH 7.12 intact), and the suppressed window does not count toward the controller Full-eviction deadline; (2) commit replays held segments at their emitting geometry (the geometry-epoch machinery, unchanged), lands the grid at target geometry, then UNDER THE SAME OutputLog LOCK ACQUISITION pushes viewers the size frame FIRST and then one synthesized sync frame (render_repaint at target geometry ++ deferred non-grid bytes) to EVERY attached sink, controller included, before raw fan-out resumes — no raw frame may interleave; (3) the controller sync frame carries watermark seq = highest suppressed seq, so the cursor-of-record advances past the whole suppressed range as-if-written (repaint supersedes; the repaint_initial watermark shape); WIRE-FLAG AMENDMENT (doyle ruling 2026-07-21, from the build's real-wire int finding — the watermark jump would otherwise fatal every STRICT consumer as an output gap and ladder into ControllerIrrecoverablyBehind = marked truncation on every resize under a live controller): OutputEvent gains additive `sync: bool` (serde default false, never serialized when false — the resume_seq D4-1 additive shape), set by the broker on the commit AND abort sync frames AND on every repaint_initial batch; a consumer accepts a FLAGGED FORWARD jump and baselines on it on BOTH dedup paths (the supersession is explicit and broker-authored — B2-sound, nothing silently skipped); FORWARD-ONLY, binding: a flagged frame at/below the cursor dedup-drops exactly as today, never a backward baseline; an UNFLAGGED jump keeps strict reject-gap byte-for-byte; baseline_next_output STAYS as the old-broker x new-client cold-attach compat path (retirement is an N-2 seed at most); N-1 matrix published: new broker x old client = a resize in the mixed-version window ends the attach with a marked truncation (honest termination, strictly better than the pre-fix silent corruption; release notes carry 'update CLI and broker together'), old broker x new client = no flag arrives, strict paths byte-identical (unit-pinned as the N-1 leg); REJECTED shapes recorded in ADR-0031 Amendment II so none is re-proposed: unconditional baseline-on-jump, empty-frame delivery through the window, seq rebase, cold-reattach-on-second-gap; (4) abort takes the same uniform path minus the size frame, at the old geometry; (5) the log records the presentation floor (seq after last commit/abort) and a resume-from-floor asking BELOW it is served the cold-attach shape (sync repaint + skip to live), never a raw replay across the boundary — at-least-once preserved in effect because the repaint supersedes the skipped range. NON-GRID BYTE DISPOSITION (enumerated at triage, every class ruled): tracked-by-grid state (title OSC 0/2, alt screen 47/1047/1049, DECTCEM 25, DECSTBM, pen, cursor) = SUPERSEDED by the repaint by construction, unit-pinned per class; untracked STATEFUL sequences (bracketed paste 2004, mouse 9/1000/1002/1003 + encodings 1005/1006/1015, focus 1004, DECCKM 1, DECAWM 7, keypad ESC=/ESC>, DECSCUSR, OSC color 4/10/11/12/104/110/111/112, charset designation), ONE-SHOT events (BEL, OSC 52 clipboard, OSC 9/777 notifications) and child->client QUERIES (DA1/DA2, DECRQM, XTWINOPS, OSC 10/11 '?') = DEFERRED verbatim in emission order, flushed inside the sync frame (a dropped toggle is indefinite divergence, a dropped query can hang a waiting child; a deferred one is bounded-window latency); cell-scoped decorations the grid does not carry (OSC 8 hyperlinks, DCS graphics) = DROPPED as a balanced class (content superseded by the repaint; an unbalanced deferred open would decorate unrelated post-repaint text; matches the repaint's existing fidelity boundary). DSR is in NO class: the drain strips + answers it pre-append (REQ-DSR-SINGLE-CPR); its mid-transition CPR reports the pre-transition cursor — accepted bounded residual, recorded not built-around. MECHANISM CONSTRAINT, binding: classification runs in the ONE parser authority — ScreenGrid's vte::Perform in a capture mode during held-byte replay, re-encoding unconsumed sequences from callback parameters; NEVER a second scanner over raw bytes (two parsers = two truths about sequence boundaries). SIDE EFFECT, must be recorded when built: the commit-time sync frame IS the seeded 'push a fresh repaint to transition-era attachers' follow-up — the cold-attach-during-transition residual's 'not bounded in VISIBILITY' caveat dies (the stale window now ends at commit); update the KNOWN ACCEPTED RESIDUAL text at repaint_initial and in REQ-RC-RESIZE-GEOMETRY-EPOCH accordingly. RESIZE_HOLD_CAP overflow now also means suppressed-never-delivered bytes: the sync repaint after a partial parse leaves client and grid sharing the same self-healing partial view — strictly more consistent than shipped; keep the overflow loud. BINDING CONSTRAINTS CARRIED FORWARD from the predecessor: NO whitespace special-casing, NO extra clears (one geometry authority for placement AND erasure — a fix that passes by clearing harder fails gate); fixture rules verbatim (readable prose, repeated words, every cell incl. expected-blank interiors, full-row untrimmed equality, no reflow-crossing at the resize instant). FENCED SEPARATE, do not build in this wave: the heuristic epoch-split strengthening (mark_resize_issued precedes session.resize; the 20ms-quiet/250ms-cap split is a heuristic, RESIZE_SETTLE_CAP admits old-geometry bytes may land in the new epoch) — only if the field stays red after the fanout repair. Gate: doc — ADR-0031 Amendment II states the presentation contract incl. the byte-class disposition table; impl — suppression + commit-time sync frame + watermark cursor advance + presentation floor in OutputLog/append/commit_resize/abort_resize; unit — per-class disposition pins (a title change, cursor-visibility toggle, DECSTBM change, alt-screen switch, AND a bracketed-paste/mouse toggle inside the suppressed window each surface in or after the sync frame; a hyperlink pair does not), the watermark advance (a resumed controller never re-receives a suppressed seq), the eviction-deadline exemption, the abort path, AND the wire-flag matrix (flagged-forward accepted + baselined on both dedup paths; flagged-backward dedup-dropped unchanged; unflagged-forward strict reject-gap byte-for-byte; old-broker wire without the key defaults false and a cold attach still baselines via baseline_next_output); int — the REAL live controller writer driven across a transition: an attached controller whose socket feed is parsed by an independent terminal authority at the CLIENT TARGET geometry, full untrimmed row equality every row after the sync frame, every cell incl. blanks; a viewer leg asserting size-frame-before-sync-frame and zero raw frames between commit and sync; a resume-across-resize leg (controller detaches pre-resize, resumes post-commit, receives zero mixed-geometry raw bytes and lands exact at target geometry). The cold-attach oracle is insufficient by construction and does not satisfy the int stage.
2026-09-06T13:09:02.0468607Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0468779Z 
2026-09-06T13:09:02.0468898Z ### REQ-ENDPOINT-MESSAGE-ONLY-DISPLAY
2026-09-06T13:09:02.0473905Z - Title: An online agent-family endpoint with NO session surface reads as message-reachable, not as a harness-hosted live agent and not as a plain ONLINE. (hertz v0.39.0 field report 2026-07-21; doyle PRODUCT RULING — deliberately NEITHER of the two options offered.) OBSERVED: an adapterless `spt ready` perch (type=ready_agent, adapter=null, ready/alive true) displays plain ONLINE with '(unknown adapter)'. SOURCE: picker/model.rs display_status returns Online for every non-live_agent type (~680-681) BEFORE consulting controllable, and amber HarnessOnly is live-agent-only (~683-687). RULING: that type gate is CORRECT and STAYS. 'ONLINE - HARNESS ONLY' means one specific thing — a LIVE AGENT whose session surface is owned by a harness rather than a broker PTY. Broadening it to 'online non-controllable agent-family' would make one label mean two different things, which is how a status label starts lying. An adapterless ready receiver is a THIRD truth: message-reachable, no session surface at all, nothing to attach to ever. So: a DISTINCT display state (working name 'ONLINE - MESSAGE ONLY') keyed on the endpoint TYPE (ready_agent), never on absence-of-adapter, and never an invented adapter name. SECOND RULING (same surface, separate lie): '(unknown adapter)' is itself a small diagnostic untruth — adapter=null is ABSENT, deliberately so, not unknown; the copy must say absent. Gate: impl — the distinct display state + the absent-adapter copy; unit — the display table gains the ready_agent row and the existing live_agent/gateway rows are UNCHANGED (this must not perturb the HarnessOnly gate), plus a label assertion for the new state.
2026-09-06T13:09:02.0478338Z - Required stages: 
2026-09-06T13:09:02.0478513Z 
2026-09-06T13:09:02.0478625Z ### REQ-LISTEN-PRESERVES-HOSTING-TOPOLOGY
2026-09-06T13:09:02.0487023Z - Title: `api listen` must not ASSERT hosting topology it does not know. (hertz v0.39.0 field RCA 2026-07-21, doyle re-grounded at source.) OBSERVED: the published adapter sequence `api bind` then identity-preserving `api listen --session-id` produces a self-contradictory live record — controlled=true AND controllable=false on a broker-hosted PTY endpoint — which controlled-precedence masks blue while attached and which a DETACH then unmasks as amber HARNESS ONLY. Detach is not the root; it only reveals the bad stamp. SOURCE: api/startup.rs passes controllable=Some(false) UNCONDITIONALLY on the relay/listen path (~191-195, reasoning 'the harness owns the process, so there is no broker PTY'), and establish_perch resolves controllable = controllable.or_else(|| prior…) (~379) — explicit wins, so that Some(false) OVERWRITES the Some(true) an earlier `api bind` EARNED. The carry-forward discipline that protects cwd/adapter/rest_state does not protect this field precisely BECAUSE the listen path is not silent about it. The defect is an ASSUMPTION about hosting authority made by a path that does not know the answer. FIX (preferred): represent LISTENER CUSTODY separately from PTY HOSTING AUTHORITY, so establishing a listener says nothing about who owns the session surface. Merely preserving controllable=true when a broker-hosted session with that session_id exists is weaker — it leaves listen guessing rather than removing the guess. Gate: impl — listen no longer asserts hosting topology for a session it does not host; unit — the stamp resolution table over (prior controllable, listen path, broker-hosted session present); int — bind -> listen -> control -> detach ends at alive=true, controlled=false, controllable=true, display ONLINE. TITLE AMENDMENT 2026-07-27 (doyle ruling, todlando build; rides the build PR per registry-mints-ride-build-PRs): THE REQ PRESERVES A **LIVING** HOSTING ARRANGEMENT ACROSS LISTENER RE-BINDS; IT DOES NOT RESURRECT A DEAD ONE'S CAPABILITY STAMP. FIELD CASE: emphasys rendered ONLINE for 25+ minutes with BOTH recorded pids dead, because its listener-only wake re-bind INHERITED a controllable=Some(true) earned in an earlier broker-PTY life, and the reconcile sweep exempts Some(true) rows from relay-death convergence (livehost.rs) — so an expired capability stamp ROUTED a liveness proof and the row was exempt from EVERY liveness model. The carry-forward is now scoped: Some(true) survives a listener re-bind unless the prior record's RELAY pid is provably Gone. Liveness of the arrangement is judged via the relay-role pid (REQ-PID-ROLE-EVIDENCE), the first record-internal key that actually measures it — NOT via 'earning pid alive', which was falsified pre-build: for a BrokerPty row the record holds no pid of the hosting life at all, only the announcing CLI's.
2026-09-06T13:09:02.0494578Z - Required stages: impl, unit, int
2026-09-06T13:09:02.0494739Z 
2026-09-06T13:09:02.0494849Z ### REQ-DIGEST-JSON-SELF-CONTAINED
2026-09-06T13:09:02.0502949Z - Title: TEARDOWN-AUTHORITY W4 (perri adapter-surface finding 2026-07-19, doyle-grounded and RE-SCOPED; title AMENDED at gate 2026-07-19 — the original welded version to --after polling, todlando falsified it from digesthub.rs/cli.rs and doyle ruled the amendment rides the wave: fifth instance of the claim-keyed-on-the-wrong-thing class, this one in the REQ registry itself): `spt endpoint digest <id> --json` must be self-contained on stdout. Today the digest snapshot version is NOT a field of the --json object at all — it exists ONLY in the DIGEST:<id> version=N trailer that cmd_digest eprintln!s at cli.rs:1619 — so a JSON consumer that wants it is FORCED to parse stderr. NUMBER-SPACE TRUTH (the amendment): version is digesthub's monotonic PROJECTION counter — it bumps when the projected digest CHANGES, serves as the --follow from_version floor and a change-detection cursor, and is NOT valid --after input; --after filters on entry seq ((ledger_ordinal<<32)|line_idx), a different number space, so a version passed as --after predates the window every time. NOTE the corrected history (perri's original RCA framed this as a fleet outage and doyle falsified it; perri confirmed): the trailer has been on STDERR since it was added 2026-06-03 (16f4c8e) and no stdout trailer ever existed, so stdout-only consumers never choked — this is a CONTRACT-COMPLETENESS defect, low priority, NOT an outage. FIX: (a) emit version as a top-level integer field inside the --json object (field name pinned by the consumer — perri's adapter parses 'version' — so no second round is needed), alongside the existing after_predates_window signal; (b) gate the stderr trailer on the non-json path so --json leaves stderr clean while the human path keeps its status line. Result: --json stdout = pure self-contained JSON including the version, safe whether the consumer reads stdout-only OR merges 2>&1. Additive to the JSON shape (REQ-CLI-JSON evolution rule). The published doc must state the number-space split with the EXPLICIT NEGATIVE (version is NOT valid --after input) — the original title proves the misuse is the natural reading. Gate: doc — the json-shapes digest section carries version, the stderr-clean --json contract, the complete entry-kind enum with per-kind produced-vs-injected provenance, and the seq/version asymmetry incl the explicit negative; impl — version field on the --json snapshot path + non-json-gated trailer; unit — the --json object carries version and stderr carries no DIGEST: trailer, the non-json path still prints it (predates ordering included), and the behavior-change sweep confirms no existing test asserts the old --json shape or the trailer presence under --json.
2026-09-06T13:09:02.0509995Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.0510151Z 
2026-09-06T13:09:02.0510247Z ### REQ-RC-DISPLAY-SOLE-WRITER
2026-09-06T13:09:02.0517405Z - Title: TEARDOWN-AUTHORITY W5 (hertz RCA 3 bug 1, 2026-07-19 — REPRODUCED on 0.38.1 CLI + 0.38.1 broker; doyle ruled, and it CORRECTS an incomplete v0.38.1 call of doyle's own): while `spt rc` owns the terminal (raw / alternate screen), rc is the SOLE writer to that display — no background thread may write to the inherited stderr. TODAY: BrainConn::split_with_reader (spt-daemon/src/brain.rs:253-270) unconditionally eprintln!s `PUMP_IPC_READER: spawned` / `exited` from its reader thread. Every reconnect attempt calls establish_attach -> Brain::cold_start_pump (rc.rs:1700-1712), and reconnect_banner_bytes (rc.rs:2040-2054) deliberately clears/homes and leaves the cursor immediately after the countdown with NO trailing newline — so the marker lands exactly at that cursor and the operator sees `Reconnecting to local daemon... 9sPUMP_IPC_READER: spawned`. Field screenshot confirms it also reaches the harness alt screen on a real `endpoint run` + `rc --take`. WHY THIS IS A CORRECTION, ON THE RECORD: v0.38.1's REQ-RC-SINGLE-PUMP-BRAIN removed the DOUBLE construction and doyle ruled the surviving single banner 'truthful, keep it' and classified the change Internal (diagnostic hygiene, not user-facing). Both halves of that ruling were wrong on facts doyle did not check: the marker is not merely a startup diagnostic, it is rendered INTO an rc-owned display, so it was user-facing all along and v0.38.1 reduced rather than eliminated the corruption. FIX: delete both unconditional eprintln diagnostics from the pump reader thread. If the observability is still wanted it goes to the daemon's persistent diagnostic sink or an explicit opt-in debug trace that NEVER inherits an interactive client's stderr — never to a stderr an attached client owns. Grounding: ADR-0043 terminal render lifecycle (one renderer owns the baseline); CONTEXT.md:33-36 (the broker's internals are not a client-visible surface). Gate: impl — diagnostics removed from split_with_reader (and any sibling unconditional client-inherited stderr write on the pump path); unit — insufficient alone and explicitly NOT the gate (the defect is cross-thread out-of-band stderr, which a banner-byte unit cannot observe); int — drive the REAL reconnect-banner path with the child's stderr captured into the SAME sink as the rendered terminal and assert neither PUMP marker appears anywhere in the captured stream, PLUS assert an ordinary initial `spt rc` is marker-free.
2026-09-06T13:09:02.0523934Z - Required stages: impl, int
2026-09-06T13:09:02.0524083Z 
2026-09-06T13:09:02.0524192Z ### REQ-TEST-LIVENESS-ORACLE-AUTHENTICATED
2026-09-06T13:09:02.0531096Z - Title: IR-15 (INFRA-REGISTER, doyle-ruled 2026-08-03): a TEST that captures a pid and POLLS it to a verdict must poll the process it CAPTURED, never the number. `provably_gone` answers 'is *a* process at this pid', which is correct for a pid just handed over and WRONG for one captured seconds earlier: between capture and verdict the OS may recycle the number onto an unrelated process, and the bare-pid oracle then reads the stranger as the captured process, still alive, until the deadline burns. Specimen: golden 30776330383, daemon::tests::a_tree_teardown_reaches_a_grandchild_the_service_spawned sat the FULL 10s window (10.176s vs 0.19/0.24s passes) on a pid-churning Phase A leg, with a Cargo-dep-pin delta that has no mechanism path to it. THE PRODUCT PATH ALREADY AUTHENTICATES pid against birth stamp (process_identity, ADR-0047 decision 1, KNOWN-HAZARDS 7.51); the TEST oracle is the half that never got the stamp -- two death oracles in one tree and the tests poll the pid-only one. FIX: a PinnedProc identity (pid + birth stamp captured at FIND time, or handed in via from_stamp where a spawn helper or a stored pid_started_at already holds one) whose provably_gone reads Absent=>gone, Present(same stamp)=>not gone, Present(DIFFERENT stamp)=>gone (reuse: ours died), Present(_)+unstamped=>not gone (degrade to bare-pid), Unproven=>not gone. POLARITY IS LOAD-BEARING AND RULED: every uncertain arm errs toward NOT GONE, i.e. toward a red, because the two failure directions are not comparable -- a false GONE hides a real kill-tree miss behind a green gate and is unrecoverable, a false NOT-GONE is a visible recoverable flake. The pre-existing property that this oracle can only ever false-RED is therefore PRESERVED, not traded: a green from a poller built on it stays exactly as conclusive as before, which is what the same-sha-rerun authorization rests on. PLATFORM STRENGTH DIFFERS AND MUST NOT BE FLATTENED: Windows creation FILETIME is 100ns so Present(different) is decisive; Linux starttime is clock ticks since boot at 10ms granularity, so a reuse landing inside one tick reads Present(same) and errs RED (the safe direction) -- this NARROWS the window and does not close it, and Present(same) on Linux is never proof the pinned process survived. Image-path corroboration (releases#120) is the second axis that closes the remainder and composes onto the SAME pinned identity. SCOPE IS THE TEST-SIDE ORACLE: one shared helper covering the swept call sites (daemon.rs teardown test + endpoint_lifecycle.rs relay_pid, the latter not yet red), never a patch to whichever test happened to fire; product-side callers (teardown.rs root_provably_gone, servicehost.rs) stay out and belong to #120's product lane.
2026-09-06T13:09:02.0538154Z - Required stages: impl, unit
2026-09-06T13:09:02.0538301Z 
2026-09-06T13:09:02.0538402Z ### REQ-LIVENESS-ORACLE-SOUND
2026-09-06T13:09:02.0551661Z - Title: TEARDOWN-AUTHORITY W2 (todlando W1 gate-round-0 finding, doyle-scoped from the LANDED W1 code 2026-07-19): 'does this pid still exist' has ONE answer in spt-core and it is derived from the OS process table. TODAY spt-daemon/src/broker.rs session_is_zombie computes wrapper_alive from spt_store::proc::is_process_alive, which probes OpenProcess on Windows — and OpenProcess keeps SUCCEEDING for a TERMINATED process while any parent holds an open handle, which the broker ALWAYS does (Arc<PtySession>) for every PTY child it spawned. A correctly-reaped harness therefore reads ALIVE, flipping zombie_verdict off its PRIMARY class (Some(false) = dead root + surviving record = always a zombie) onto the conditional arm, which additionally demands adapter_labeled && past_grace && !has_live_descendants. CONSEQUENCE, live today: a dead-root session that is NOT adapter-labeled is claimed LIVE indefinitely — `endpoint run`'s dup-guard refuses ENDPOINT_ALREADY_LIVE over an already-dead tree and cmd_rest's Suspend alive_hint forces from=alive on the same false claim (both via has_live_session_honest, cli.rs:2014 and :3805). REQ-ENDPOINT-CYCLE-HONEST exists to give the cycle verbs ONE liveness authority; after W1 there are TWO and they disagree by construction (teardown.rs::root_provably_gone asks the table and is right; session_is_zombie asks is_process_alive and is wrong). THE DISCRIMINATOR (binding, and it makes the audit checkable rather than 20 judgement calls): is_process_alive is unsound EXACTLY when the ASKER — or a live ancestor — still holds an open HANDLE to the target; dropping a Child closes it, so a spawner that DROPS is honest and one that RETAINS is not. Hence the dangerous shape is asking 'is it GONE' about a process you OWN, and the SAME CALL IS SOUND IN THE CLI AND UNSOUND IN THE DAEMON FOR THE SAME PID — soundness is a property of the asker, not the call. REJECTED ALTERNATIVE, recorded in ADR-0045 Amendment 1 so it is not re-proposed: proc::reap_if_child before the probe fails twice — it is a NO-OP on Windows (#[cfg(windows)] let _ = pid), and decisively the broker holds the handle BY CONSTRUCTION for every pid this predicate is ever asked about, so no handle-based probe can EVER be sound at this site. FIX: (a) add a sound probe as a NAMED SIBLING whose name is the question — process_exists(pid) over process_table() — and state on is_process_alive's own doc which question it answers and which it does not, pointing at the sibling (the behavior was documented VERBATIM in legacy_resident_sweep_e2e.rs since 2026 and never reached proc.rs or zombie_verdict: written where DISCOVERED, not where CONSUMED); (b) route session_is_zombie's wrapper_alive through it, leaving zombie_verdict PURE and UNCHANGED (it was fed a lie, it is not wrong); (c) an EMPTY table is NO KNOWLEDGE — it must resolve to None (zombie_verdict(None) already means 'never guess') and must NEVER manufacture Some(false), which would mass-classify every live session a zombie = the W1 blocker inverted at broker scope; (d) AUDIT, do not mass-migrate, the remaining callers using the discriminator — roster.rs/api/startup.rs/api/auth.rs are already adjudicated SOUND (the asker never spawned the target); cli.rs purge psyche-quiesce is RIGHT ANSWER FOR A FRAGILE REASON (sound only because the CLI asks and the DAEMON spawned the psyche — it goes unsound SILENTLY if that check ever moves into the daemon) and gets a comment naming the asker as what makes it safe; the daemon-side population (livehost.rs, shellhost.rs, shellwake.rs, lifecycle.rs, in-daemon is_perch_alive) is adjudicated one verdict per site (sound/unsound/unreachable), fixing only unsound AND reachable and REPORTING the verdicts even where left — if unsound-and-reachable exceeds a handful, STOP and escalate to doyle rather than widening this wave. Gate: doc — KNOWN-HAZARDS 7.50 + ADR-0045 Amendment 1; impl — process_exists + the caveat on is_process_alive + wrapper_alive routed + the adjudicated fixes; unit — the empty-table arm resolves to None (never Some(false)), a handle-held corpse classifies as a zombie, and zombie_verdict's existing pure table is extended with the previously-unreachable case (dead root + not adapter-labeled + within grace); int — manufacture the REAL handle-held-corpse condition (broker retains a handle to a killed PTY child) and assert the session classifies zombie + the run dup-guard does NOT refuse over it; Windows-only if it cannot be made on Linux, and the test NAME says so.
2026-09-06T13:09:02.0563182Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0563387Z 
2026-09-06T13:09:02.0563511Z ### REQ-SHELL-FRAME-VOCAB
2026-09-06T13:09:02.0568450Z - Title: The shell relay frame vocabulary is a PUBLISHED contract, not a reverse-engineered one. (flynn spt-alchemy clean-room audit 2026-07-21, doyle code-verified, seed pair item 1.) TODAY: shellchan.rs composes the exact frames a shell binary must parse — shell_command / shell_text / shell_file (+ shell_close, sensory, drive) — but the published export (docs-site/src/shells/) carries ZERO occurrences of those type names; the docs say only 'the shell child parses its own vocabulary', so every adapter author (notify-shell, alchemy) reverse-engineers the frame shapes compatibly from source. FIX: publish, in the shells section of the docs-site export, the frame type names + their attrs (op, xfer-id, path) + body encodings — a shell_command body is a JSON object of named args (positionals zipped against the manifest's declared arg names), a shell_text body is the raw text, a shell_file body is the original filename with the perch-relative landed path in the path attr, a shell_close body is the manifest's pre_close instruction (NOT vocabulary-checked — the vocabulary gates agent commands, the manifest is its own authority over its own binary). Fold in the quoted-composite-tail sharp edge: the vocabulary check REFUSES extra positionals (an op given more args than its declared arity errs, so a quoted composite tail must be ONE argv element). Docs-drift-gated like every published page. Gate: doc — the shells export carries the full frame vocabulary (type names, attrs, body encodings, shell_close) + the files/ landing contract (published together with REQ-SHELL-PERCH-DIR's key so the path attr is resolvable the day it is documented) + the arity sharp edge.
2026-09-06T13:09:02.0572962Z - Required stages: doc
2026-09-06T13:09:02.0573104Z 
2026-09-06T13:09:02.0573193Z ### REQ-SHELL-PERCH-DIR
2026-09-06T13:09:02.0578763Z - Title: A shell binary can mechanically resolve where its files land. (flynn spt-alchemy clean-room audit 2026-07-21, doyle code-verified, seed pair item 2, P1 — HARD-GATES flynn's alchemy W4.) TODAY: `spt shell send --file` lands the blob at <shell-perch>/files/<xfer-id>-<name> and the shell_file frame's path attr is PERCH-RELATIVE (files/...) — but the spawn template substitution keys are ONLY {id}/{adapter_name}/{link_token} (shellhost.rs fill_spawn_command) and the spawned child inherits the BROKER's cwd, so no mechanical perch resolution exists: the binary cannot turn the frame's path into a real file without guessing SPT_HOME layout. DOYLE RULING, both halves binding: the frame KEEPS the perch-relative path (an absolute path in a spooled frame LIES across perch moves and node boundaries — frames outlive layouts); the fix is an ADDITIVE {perch_dir} spawn-template substitution key (opt-in — templates that do not use it are byte-identical, N-1-safe by construction) filled with the shell perch dir so the binary receives its root at spawn and joins the frame's relative path against it. REFUSED, recorded so it is not re-proposed: blessing SPT_HOME layout guessing as an interim contract. Published landing contract rides REQ-SHELL-FRAME-VOCAB's page. Sequence-not-version given to flynn (no version floor before it ships). Gate: doc — the {perch_dir} key on the manifest spawn-template docs + the files/<xfer-id>-<name> landing shape on the frame-vocab page; impl — fill_spawn_command passes perch_dir alongside id/adapter_name/link_token; unit — the filled template carries the perch dir as ONE argv element (the multi-word/space path shape the argv-fill hazard exists for) and a template WITHOUT the key is byte-identical pre/post (the N-1 guarantee); int — a real spawned shell binary given {perch_dir} resolves a shell_file frame's perch-relative path to the landed blob and proves the read.
2026-09-06T13:09:02.0584007Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0584178Z 
2026-09-06T13:09:02.0584283Z ### REQ-CI-POSTJOB-DAEMON-REAP
2026-09-06T13:09:02.0592987Z - Title: A CI job REAPS ITS OWN test-spawned daemons at battery end, in-job, and logs a process census at job start AND job end so contamination and reap effectiveness are visible in every run's log. (Load-flake family leg 1, doyle-ratified 2026-07-22 from deployah's third-run analysis.) THE SIGNATURE THIS CLOSES: a DIFFERENT single daemon-spawning test dying per run with a bare exit 1 and NO assertion output — process-level death, not a failed assert — while sibling tests in the same families pass alongside it, on BYTE-IDENTICAL code. Evidence: release PR #56 ran four times over a zero-.rs-delta tree; runs 1/2/3 killed brain_decouple (twice, on a disk-starved box), then adapter_translate, then adapter_digest at 105.9 GB free; box census during runs showed 43 live spt-family processes and 6486 handles against an 1881-test Phase-A full-parallel battery; run 4 went GREEN once disk and leaked session-0 daemons were cleared. WHY IN-JOB IS LOAD-BEARING AND NOT A CONVENIENCE: there are TWO leak populations on hfenduleam. Population A is session-1 (agent/gate-spawned) and is sweepable by path from any shell. Population B is SESSION-0, spawned by the actions.runner.* service — a session-1 shell CANNOT kill those (Access denied; ExecutablePath unreadable) even though they are healthy. Every CI run therefore leaves session-0 daemons behind that contend with the NEXT run while also leaking its own mid-run, and an external sweep can never reach them. The runner's own job context owns its session-0 children, so only a post-job step inside the job can reap them. KILL SCOPE IS NARROW AND PATH-VERIFIED PER-PID AT KILL TIME (never machine-wide, and never trusting the census snapshot, whose pids can be recycled): eligible only under the run's own build roots — CARGO_TARGET_DIR, the workspace target, the notify-adapter checkout's target, RUNNER_TEMP, and the pinned n1 old-broker build cache. TWO HARD EXCLUSIONS are checked AFTER the root test rather than instead of it, so that live infra survives a bug in the root computation: anything under an spt-core/bin/ install prefix, and any owl binary. A process whose image path cannot be READ is reported but NEVER killed — unreadable means unverifiable and the safe direction is to leave it standing. A BOUNDED SETTLE precedes the kill pass so a cleanly-exiting daemon is not counted as a leak; after it, a survivor is a leak by definition, which is what makes the strict-mode trigger a mechanism rather than a judgement call. Rides BOTH the test and n1-gate jobs (doyle scope ruling): n1_pairing spawns real daemon trees from the workspace and from the out-of-tree pin cache, so reaping one job leaves half the cause standing. Gate: impl — the two census/reap scripts under .github/ci/ plus their job-start and always() job-end wiring on both jobs, both runners. Kin REQ-CI-WINDOWS-PHASE-A-BOUND (the other cause-side leg), REQ-CI-DOCS-ONLY-THIN (recipe-layer precedent: impl-only, no product code).
2026-09-06T13:09:02.0600693Z - Required stages: impl
2026-09-06T13:09:02.0600833Z 
2026-09-06T13:09:02.0600932Z ### REQ-CI-PRERELINK-EXE-LOCK-REAP
2026-09-06T13:09:02.0609250Z - Title: NO CI step that RELINKS a workspace binary runs while an earlier phase's leaked binary is still executing from the same target. (BAROMETER golden triage, hertz RCA 2026-08-02, releases#123.) THE DEFECT THIS CLOSES IS PLACEMENT, NOT MECHANISM: REQ-CI-POSTJOB-DAEMON-REAP's reap already kills exactly the right processes with exactly the right scoping, but it is the LAST step in the job, so it runs after every step it would have saved. Windows cannot overwrite a running image, so a leaked `spt.exe` from Phase A/B makes the next `cargo test -p spt --test <x>` die with 'failed to remove file ...\target\debug\spt.exe: Access is denied. (os error 5)'. EVIDENCE, twice out of twice on the same head: golden 30754060525 and 30758010357, both Windows `test` jobs — job-start census scoped=0 (so the leak is generated IN-job, not inherited), pre-reap census scoped=4 with the same handle-count signature both runs (two ~258-260 daemons + two ~134 brains), job-end reap killing 4 and 3 respectively AFTER the job had already died of them. The second run also shows the masking: run 1's Phase-A red hid this, and every run that survives Phase A dies at the notify relink instead. THE FIX IS CLASS-SHAPED: the guard rides EVERY relink-after-an-E2E edge (notify, installer, docs-drift, and the two-host gated-CLI step in twohost-a), not only the edge that reddened a run. ONE PREDICATE OVER ONE POPULATION: it is the same reap-census script under a `prerelink` phase, so a mid-job reap can never classify a process differently from the job-end reap; the pre-existing inline `Get-Process | Stop-Process` before the docs gate is REPLACED by it rather than left as a second, weaker rule (it scoped by GITHUB_WORKSPACE alone, missing CARGO_TARGET_DIR/RUNNER_TEMP/n1-cache roots, re-verified nothing at kill time, and printed no evidence). Census labels carry the phase (`prerelink-pre-reap`, `prerelink`) so a mid-job census is never counted as an end-of-job one by the strict-mode trigger. STRICT STAYS AN END-OF-JOB MECHANISM: a prerelink survivor warns and never reds its own step, because the relink it guards is the very next step and fails with the exact file it could not remove — a louder and more specific signal than a reap step exiting 1. WINDOWS-ONLY BY CONSTRUCTION: Linux replaces a running exe happily; role B (Linux) of the two-host pair needs no guard. Gate: impl — the `prerelink` phase in .github/ci/reap-census.ps1 plus its four wirings in golden.yml. No unit stage, for REQ-CI-POSTJOB-DAEMON-REAP's stated reason: the evidence is a recipe the runner executes and the acceptance is read off the run log (a `CI-REAP-PRERELINK summary:` line before each relink, and no os-error-5). Kin REQ-CI-POSTJOB-DAEMON-REAP (same machinery, the other placement), REQ-CI-WINDOWS-PHASE-A-BOUND (the co-tenant load leg).
2026-09-06T13:09:02.0616498Z - Required stages: impl
2026-09-06T13:09:02.0616640Z 
2026-09-06T13:09:02.0616750Z ### REQ-CI-WINDOWS-PHASE-A-BOUND
2026-09-06T13:09:02.0626299Z - Title: Phase-A nextest parallelism is BOUNDED on the Windows self-hosted leg, so the full-parallel 1881-test battery stops saturating a box that also hosts the live agent fleet. (Load-flake family leg 2 = deployah option (c), doyle-ratified 2026-07-22.) Windows leg ONLY: kitsubito does not host the fleet, and its parallelism is deliberately untouched. MECHANISM AND WHY THIS ONE: the bound lives in a [profile.ci-windows] nextest profile selected by a STEP-LEVEL NEXTEST_PROFILE expression on the Phase-A step, NOT by a per-OS pair of Phase-A steps — duplicating that step would put a FOURTH copy of the <HEAVY> filter string in the tree, and .config/nextest.toml's own header names keeping THREE in sync as the standing hazard. THE NON-INHERITANCE GATE-CHECK (doyle demanded it discharged, and it did not come back clean): a custom nextest profile inherits scalar settings from default but NOT profile.default.overrides — the argument cuts both ways. Of the five overrides on default, exactly ONE is kind(test): the by-binary <HEAVY> integration filter, which is Phase B's alone and needs no mirror. The other FOUR are the FLAKE-LEDGER: attach_viewport_reconnects_across_a_broker_bounce class — real-broker units that are kind(bin)/kind(lib), escape the by-binary filter, and therefore RUN IN PHASE A depending on the heavy-broker-pty group to stay serialized (rc::tests four names, (applyhost|livehost|pump)::tests, wansend::tests, api::startup::tests). Shipping the profile without them would have SILENTLY UN-SERIALIZED ON WINDOWS the exact class whose un-serialization produced two separate 240s timeouts. They are mirrored verbatim, and the parity is ENFORCED rather than remembered: xtask check's check_phase_a_profile_parity fails the build when a default override that is not kind(test) is missing from ci-windows, keyed on that SHAPE and not on a name list — the same posture as that ledger entry's second half, whose whole lesson is that a class written in prose and shipped as an enumeration decays. A DRIFTED mirror reads as missing, not as present: the invariant is that it is the SAME filter in both profiles, not that something is in both. THE BOUND ITSELF IS CHOSEN, NOT DERIVED, and is recorded that way at the value: hfenduleam is 16 logical cores and the bound is 8 (half), for fleet-co-tenant headroom. No measurement separates 8 from 6 or 12 and none was run — the ratified fix is stop saturating the box, not find the saturation knee. Retuning is one integer. Gate: impl — the ci-windows profile with its mirrored overrides plus the ci.yml step-level profile selection plus the xtask parity check; unit — the parity predicate's exemption rule (a kind(test) override needs no mirror), its detection of an unmirrored Phase-A override, its rejection of a DRIFTED mirror, and a case over the REAL checked-in config so the check cannot ship green against a tree that already violates it. Kin REQ-CI-POSTJOB-DAEMON-REAP (the other cause-side leg), REQ-HEAVY-UNIT-CLASSIFICATION (the check one layer in), FLAKE-LEDGER: attach_viewport_reconnects_across_a_broker_bounce + the HEAVY-AT-BIRTH ruling (.config/nextest.toml).
2026-09-06T13:09:02.0634565Z - Required stages: impl, unit
2026-09-06T13:09:02.0634717Z 
2026-09-06T13:09:02.0634818Z ### REQ-CI-LOAD-BEARING-PATCH-PIN
2026-09-06T13:09:02.0643088Z - Title: A `[patch.crates-io]` pin that FIXES A SHIPPED DEFECT is asserted still in force by `xtask check`, in BOTH of the two ways it can silently lapse. (INFRA-REGISTER IR-4 part 1, releases#125 fix-lane intake; hertz 2026-08-03.) THE PIN AT RISK: swarm-discovery 0.6.0-alpha.2's `updater::gc` answers a send to a stopped actor by scheduling another gc task 10ms later — a ~100 Hz retry loop per destroyed updater that can never reach a live receiver, so every updater recreation leaks one more loop. Latent in EVERY daemon since v0.38.1; the field cost was a kitsubito daemon burning two cores for two weeks (~9 accumulated loops). Fixed upstream in rkuhn/swarm-discovery PR #27, which is in NO crates.io release (0.6.3 predates it) and iroh pins swarm-discovery to EXACTLY 0.6.0-alpha.2, so no published version satisfies the graph — the tree carries a minimal-delta backport fork pinned by `rev`, i.e. content-addressed. TWO FAILURE MODES, WHICH IS WHY ONE ASSERTION IS NOT ENOUGH: (1) a routine `cargo update` or an iroh bump re-resolves Cargo.lock back to the registry crate while the stanza still sits in Cargo.toml looking correct — the lock disagrees with the manifest, or resolves from the registry outright; (2) the STANZA IS DELETED, after which there is nothing left for the lock to disagree with and a lock-only check reads clean over a reintroduced defect. So the manifest is asked whether the pin exists AT ALL, and only then is the lock asked whether it agrees. THE EXPECTED REV IS READ FROM THE MANIFEST, never duplicated into xtask: a third copy of the sha is one more thing to forget, and a legitimate re-pin would then red on the wrong file. What IS hardcoded is the crate plus the reason its patch is load-bearing — the judgement a lock file cannot carry and a human needs in order to decide whether dropping it is safe — and the refusal PRINTS that reason plus its own drop condition (upstream ships PR #27 AND iroh's pin reaches it), so the guard states when it should be removed rather than becoming permanent by default. Removing a crate from the guard list is therefore a reviewed edit in the same commit, not a silent lapse. Gate: impl = LOAD_BEARING_PATCHES + check_load_bearing_patch_pins wired into `xtask check` (already a golden docs-drift step on both OS legs, so this rides an existing leg rather than adding one); unit = the three failure modes each proved separately (deleted stanza, registry resolution, moved rev), the two parser traps that would make the guard answer for the wrong thing (a prefix-named crate, the trailing `[[patch.unused]]` table), and an arm that runs the real workspace so the guard cannot ship green against a tree it does not actually hold. Kin REQ-CI-WINDOWS-PHASE-A-BOUND, REQ-HEAVY-UNIT-CLASSIFICATION (same `xtask check` seam: a rule that outlived the memory it depended on).
2026-09-06T13:09:02.0650527Z - Required stages: impl, unit
2026-09-06T13:09:02.0650670Z 
2026-09-06T13:09:02.0650776Z ### REQ-LOCK-TOUCHING-LANE-PROCEDURE
2026-09-06T13:09:02.0656986Z - Title: A lane that changes `Cargo.lock` follows a written procedure whose review step reads EDGES, not just the package set. (INFRA-REGISTER IR-4 part 2; hertz 2026-08-03.) THE HUMAN HALF of REQ-CI-LOAD-BEARING-PATCH-PIN: the guard catches a pin that lapsed, and this catches the re-resolve that lapses it — a `cargo update` with no `-p` re-resolves the whole graph, so a one-crate intent ships as an unreviewed graph move. THE SPECIMEN IS IN THIS TREE: at acaaa4f the `[[package]]` SET was identical before and after — no block added, none removed, not one `name`/`version` line in the diff — while EIGHT blocks had their `windows-sys` edge moved from 0.61.2 to 0.59.0 (consumer counts 18→10 and 5→13; both version blocks survive, so even a version-presence check reads clean). The commit message states 'changed one package and left 132 dependencies untouched', which is true of the set and false of the edges: the reviewer who counts blocks is told nothing happened. Hence the rule states BOTH reads as separate steps, because the cheap one alone is the one that lies. TOOLCHAIN DRIFT IS THE OTHER READING of a surprising diff — two resolutions of the same manifests that disagree are evidence about the resolvers, not about the dependencies, so the procedure sends the author to the golden toolchain-version print (REQ-CI-TOOLCHAIN-VERSION-PRINT) before shipping either lock rather than picking the diff they like. A HAND-EDITED LOCK IS ALLOWED and given a mechanical acceptance test — `cargo check --workspace --locked` — where `--locked` is the load-bearing half: without it cargo silently repairs the file it was asked to verify, which is the same class as running `cargo run` against a build-file mutation you are trying to measure. Gate: doc = the `Lock-touching lanes` section of docs/GOLDEN-CI.md (targeted update, the two-read review with its specimen, the drift stop-condition, the `--locked` acceptance test, and the `xtask check` pin guard as the lane's own preflight). Kin REQ-CI-LOAD-BEARING-PATCH-PIN (mechanical half), REQ-CI-TOOLCHAIN-VERSION-PRINT (the comparison this procedure sends you to).
2026-09-06T13:09:02.0662631Z - Required stages: doc
2026-09-06T13:09:02.0662762Z 
2026-09-06T13:09:02.0662877Z ### REQ-CI-TOOLCHAIN-VERSION-PRINT
2026-09-06T13:09:02.0669860Z - Title: Every golden run PRINTS the toolchain that judged it, on BOTH OS legs, from inside the job. (INFRA-REGISTER IR-4 part 3, carrying IR-9; hertz 2026-08-03.) WHY A PRINT IS EVIDENCE AND A BOX IS NOT: both golden boxes own rustup-managed toolchains that drift independently, so 'did CI resolve this the way I did?' — the question the acaaa4f lock comparison needed answered — had NO answer in any run log. MEASURED on the two boxes' INTERACTIVE accounts 2026-08-03: hfenduleam (Windows leg) cargo/rustc 1.93.0, clippy 0.1.93; kitsubito (Linux leg) cargo/rustc 1.96.0, clippy 0.1.96 — three minor versions apart, which is IR-9's class with numbers on it. That measurement is a PRIOR, not the fact: a runner service account's toolchain is what judges a lane and an interactive shell on the same box does not prove it, which is precisely why the step prints from inside the job. Direction of the skew is deliberately NOT asserted — clippy renames and removes lints as well as adding them, so 'newer is stricter' is unmeasured. IR-9's docs half (align the boxes, or declare one authoritative clippy leg) is HELD by doyle's ruling of the same day: rule it once this step reports runner-account versions, rather than on the prior. STEP SHAPE: one `TOOLCHAIN ` line prefix on every line of both legs so a run's toolchain is one grep; cargo, rustc, and clippy versions are the load-bearing three and their absence reds the step; the rustup active-toolchain line is a convenience and its absence must NOT red a step whose whole job is to print — which needs a different construction per shell, and both were exercised before shipping rather than reasoned about. Windows: under GitHub's pwsh wrapper (`$ErrorActionPreference = 'stop'` + a trailing `$LASTEXITCODE` check) a MISSING COMMAND is a terminating error, so rustup's absence is TESTED with Get-Command rather than caught, and a present rustup's non-zero exit is cleared before the wrapper reads it. Linux: no `| head` anywhere — under `set -o pipefail` a producer SIGPIPEd by an early-exiting head surfaces as exit 141 and reds the print, so first-line trimming is parameter expansion. Gate: impl = the two `Toolchain versions` steps in golden.yml's `test` job (the job that builds, tests, and lints, so the one whose resolver and lint set a reader is asking about). Kin REQ-LOCK-TOUCHING-LANE-PROCEDURE (the procedure that sends an author here), REQ-CI-LOAD-BEARING-PATCH-PIN.
2026-09-06T13:09:02.0675967Z - Required stages: impl
2026-09-06T13:09:02.0676106Z 
2026-09-06T13:09:02.0676209Z ### REQ-CI-LINK-HEALTH-PROBE
2026-09-06T13:09:02.0684107Z - Title: The shared-runner quiet predicate gains a NETWORK axis: both two-host jobs measure the link to their peer immediately before rendezvous, print it, and carry it in the bench ledger. (INFRA-REGISTER IR-1; hertz 2026-08-03.) WHY THE EXISTING PREDICATE CANNOT SEE THIS: its two axes are process-shaped (zero non-terminal CI runs; no local cargo/rustc/nextest by parent chain), and both PASSED on run 30771155390 — a run whose link took 321s for a 1s checkout with bidirectional 10s QUIC dial timeouts, PUMP_PEER_FAIL moving a 0→3→0 and b 8→22→8 across green/red/rerun. A network-shaped red was triaged as a product one because nothing in the run recorded the network. kitsubito is wifi-only INDEFINITELY (operator-confirmed 2026-08-02), so the link cannot be hardened and the predicate must SEE it. JITTER IS THE SIGNAL, NOT THE MEDIAN: the motivating link mostly worked, so the probe takes 5 samples and records BOTH `link-rtt-med-<peer>` and `link-rtt-max-<peer>` rows — measured while building this, an idle link ran 10..83ms on one direction and 3..75ms on the other, i.e. the spread is the whole datum and a median alone reads healthy through the failure. ALSO CARRIED: DERP-vs-direct path (a relayed rendezvous is a different network than the one the rig's timings were tuned on), sample loss, and the peer's resolved IP so a reader can check it against the rig's own SPT_TWO_HOST_PEER_IP. AN INSTRUMENT, NOT A GATE — always exits 0, no threshold refuses a rendezvous: whether a number should is an unmade ruling, and a probe that reds a run converts 'the link was slow' into 'the suite failed', which is the confusion it exists to remove. A FAILED PROBE IS A LABELLED HOLE, NEVER SILENCE: no tailscale on PATH or no reply still writes an `ok:false` row, because 'attempted and did not land' is a different claim from 'never probed'. The absence of the CLI is TESTED rather than caught on the pwsh side (under GitHub's `stop` preference a missing command is terminating), and the bash side uses no `| head` (pipefail + SIGPIPE = 141 on an instrument). Gate: impl = .github/bench/link-probe.{sh,ps1} + the probe step in twohost-a (peer kitsubito) and twohost-b (peer hfenduleam), both exercised on the REAL boxes before shipping — five-sample success, no-reply, and absent-CLI arms on each shell, the absent-CLI mutation counted before it ran after a first attempt silently re-measured the unmutated arm; doc = docs/GOLDEN-CI.md 'The link is a third axis' (how to read the LINK line, what relay/loss/jitter each mean, and that the probe does not gate). Kin REQ-BENCH-STEP-LEDGER (the row shape these writers share, and whose parity gate now DISCOVERS its writers instead of listing them), REQ-CI-FREE-SPACE-PREFLIGHT (the same predicate's disk axis, amended the same day).
2026-09-06T13:09:02.0691209Z - Required stages: doc, impl
2026-09-06T13:09:02.0691359Z 
2026-09-06T13:09:02.0691458Z ### REQ-RESUME-CUSTODY-IDENTITY
2026-09-06T13:09:02.0695650Z - Title: Resume custody is an identity pair (pid + process creation time), never a bare PID. (ADR-0047 decision 1; hertz v0.39.4 field bug 1, RCA accepted 2026-07-22.) TODAY: livehost's restart gate and liveness-reconcile DEFER both consume `read_resume_pid(..).is_some_and(is_process_alive)` — zero identity binding, so a dead wake-resume spawn's pid recycled onto an unrelated process (field: resume.pid=29456 -> cmd.exe) reads as a live resume forever: reconcile defers every tick, the row stays online-authoritative, FALSE-ONLINE with no self-repair. FIX: the custody record stores (pid, creation_time) written atomically at spawn-mint; every consumer tests the PAIR; mismatch = NOT OURS -> the discovering reader DELETES the record and proceeds (self-heal, not error); successful bind and spawn-reap clear custody atomically with their own outcome. Creation time from the process SNAPSHOT, never a retained handle (KH 7.50); platform without a snapshot -> unproven -> defer one tick, never a manufactured verdict. Gate: impl — the paired custody record + both livehost consumers on the pair test; unit — pair mismatch reads NOT-OURS + record deleted, pair match reads OURS, absent-snapshot defers; int — the recycled-pid rig: mint custody, kill the spawn, occupy the pid with an impostor, prove reconcile deletes custody and the row goes honest (the bug-1 field shape, RED-first against the bare-PID gate).
2026-09-06T13:09:02.0699480Z - Required stages: impl, unit, int
2026-09-06T13:09:02.0699640Z 
2026-09-06T13:09:02.0699746Z ### REQ-ENSURE-DAEMON-STOP-INHIBIT
2026-09-06T13:09:02.0704705Z - Title: An operator stop outranks every implicit daemon ensure. (ADR-0047 decision 2, AMENDING REQ-DAEMON-3's anchor; hertz v0.39.4 field bug 2, RCA accepted 2026-07-22.) TODAY: every `spt api` invocation runs unconditional ensure_daemon() (api/mod.rs run()), so on a box with live adapter sessions a `daemon stop --force` loses the race to hook-driven api calls — respawn convoy (5-10 ephemeral spawner windows), several stops to stay down; the rc-side twins were fixed earlier, the api anchor stayed armed. FIX: `daemon stop` records a durable machine-scoped STOP INHIBIT before teardown begins; ensure_daemon()/ensure_running() consult it and DECLINE with one honest line naming the remedy ('daemon stopped by operator — spt daemon start to resume'); cleared by intent verbs ONLY (explicit `daemon start`; update paths that restart by design) — NO TTL (rejected: a timeout is the surprise respawn again, later); implicit autostart additionally takes a machine-wide lock around probe-and-spawn so N concurrent callers never launch N daemons. Gate: doc — the stop/start contract on the daemon CLI docs (stop now sticks; the refusal line + remedy named); impl — inhibit mint in cmd_stop + consult in both implicit anchors + clear in daemon start/update-finish + the spawn serialization lock; unit — inhibit present -> ensure declines with the message, absent -> spawns, intent verbs clear, non-intent paths never clear; int — the convoy rig: stop under a concurrent api-call storm -> daemon stays down + zero respawns + refusal printed, then explicit start clears and exactly ONE daemon comes up under the same storm (RED-first against today's anchor).
2026-09-06T13:09:02.0709239Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0709406Z 
2026-09-06T13:09:02.0709511Z ### REQ-ATTACH-IDEMPOTENT-REPLAY
2026-09-06T13:09:02.0714908Z - Title: The equal-generation lease rung is idempotent for the same connection. (ADR-0047 decision 3, AMENDING ADR-0044's ladder inside the equal-gen rung; hertz v0.39.4 field bug 4, PINNED via OBS breadcrumbs on authorized same-seam `daemon refresh` 2026-07-22 — gen+1 premise FALSIFIED.) TODAY: a Control/Take subscribe with same identity + same nonzero gen classifies 'same lease, silent re-take' (broker.rs equal-gen branch — correct, no revoke) but re-take = become_controller, which unconditionally takes+drops the prior seat (writer exits channel-closed) with NO same-conn check — designed for the dead-seat dispatcher-restart successor, it also fires against the SAME LIVE conn re-served 15ms apart by post-cycle dispatcher replay: the lease kills its own writer, the rc viewer freezes until detach+re-attach (the field 'update freezes PTYs'). FIX: keyed (endpoint/session, by, conn, gen) — same-conn equal-gen = IDEMPOTENT REPLAY: seat + writer PRESERVED, no controller-replaced, no second initial batch; breadcrumb answers decision=idempotent (additive vocab). Equal-gen DIFFERENT-conn keeps today's silent swap (the ADR-0038 fix-6 successor — must not regress); strictly-newer keeps loud supersession, strictly-older keeps busy. BANKED open question (build-time look, not a blocker): why the dispatcher double-served one held attach 15ms apart — may have siblings. Gate: impl — the same-conn discriminator in resolve_subscribe's equal-gen branch + the idempotent decision label; unit — the four ladder cells (same-conn idempotent / different-conn swap / newer supersedes / older busy) against a mock seat; int — the bug-4 rig: replay one held Attach after a brain refresh, assert ONE writer + zero controller-replaced + the original rc interactive (RED-first), plus the different-conn successor leg stays green (redispatch/brain_swap suites).
2026-09-06T13:09:02.0719828Z - Required stages: impl, unit, int
2026-09-06T13:09:02.0719995Z 
2026-09-06T13:09:02.0720099Z ### REQ-HAZARD-RESUME-CUSTODY-ABA
2026-09-06T13:09:02.0721584Z - Title: KNOWN-HAZARDS 7.51: process custody is an identity, never a bare PID — a recycled pid must read NOT OURS. The hazard-conformance twin of REQ-RESUME-CUSTODY-IDENTITY: its int-stage recycled-pid rig IS this hazard's required test (custody pair mismatching a live impostor pid -> record deleted, reconcile proceeds, row goes honest). Registered separately so the hazard list stays a conformance checklist (CLAUDE.md rule 4) — evidence may tag the same rig.
2026-09-06T13:09:02.0722885Z - Required stages: doc, int
2026-09-06T13:09:02.0723024Z 
2026-09-06T13:09:02.0723125Z ### REQ-HAZARD-BIND-CONFLICT-PID-ABA
2026-09-06T13:09:02.0734719Z - Title: THE BIND CONFLICT CHECK MUST ASK CUSTODY, NOT A BARE PID (KNOWN-HAZARDS 5.1/7.51 -- "custody is an identity, never a bare PID"; releases#134, FOUND AND ROOT-CAUSED BY hertz including the measured chain and the discriminating negative control). `establish_perch`'s under-lock conflict check read the recorded owner pid and asked `is_process_alive` with NO birth pairing, while `pid_started_at` sat unread in the SAME record and the shipped paired resolver (`liveness::relay_liveness`) was already in use 260 lines earlier in the same file (`prior_relay_died`). DECLARE SITE != ENFORCE SITE: the ABA fix landed on the relay-death and resume-custody paths and was never aimed here. CONSEQUENCE, MEASURED IN THE FIELD on the `hertz` endpoint (specimen preserved, sha256 1d8805ef841a1c1fe930b23eb8ce1683115e33735e190e426dd3ab3416ae3b1e): recorded pid 30576 with birth 2026-08-01T08:11:26.027Z, the pid recycled onto an unrelated process at 12:34:04.317Z, so every subsequent bind was refused CONFLICT and the row sat `unbound` FOREVER -- ready=false, alive=false, not addressable inbound, with `resume_unbound_stamp` re-arming a bind that could never succeed. It fails SILENTLY rather than loud, and Windows recycles pids aggressively, so it is latent fleet-wide rather than endpoint-specific. THE FIX PAIRS THE RECORDED BIRTH INTO THE CHECK, and both facts come from ONE read -- the under-lock `prior` snapshot -- replacing a second file read that could pair a freshly-read pid against `prior`'s session_id. UNPROVEN FAILS TOWARD ALLOW HERE, INVERTING `relay_liveness`'S OWN DEFAULT, and the inversion is the requirement rather than an implementation choice: that default exists so a HEAL is never mis-fired, but at this site "alive" means REFUSE THE BIND, so carrying it over converts an unknown into the same permanent lockout through a different input. MEASURED CORRECTION TO THE RULING'S OWN JUSTIFICATION (todlando recon, 2026-08-03): this does NOT confine the behaviour change to `Gone`. `process_identity` also answers `Unproven` for a LIVE pid whose birth cannot be read (a denied query, an exit race, or a platform with no birth oracle -- `process_started_at` is documented `None` on non-Linux unix), and that arm refuses today and allows after. The case given up is bounded by the owner's own exit race; the case fixed is permanent. THE FIX RESTS ON THE BIRTH PAIRING, NOT ON `pid_role`, AND THAT MUST BE PROVED RATHER THAN ASSERTED: the field record carries `pid_role: "binder"`, so a `pid_role == Relay` gate copied from `prior_relay_died` would ALSO have unblocked the reported case -- two mechanisms, one symptom. The pairing is load-bearing because a `pid_role = Relay` record whose pid is recycled is bricked identically and a role gate cannot fix it, so the requirement carries a Relay+recycled row specifically to prove the role gate is not carrying the fix. NO ROLE GATE IS ADDED HERE: it would change behaviour on `Held` binder records too, far wider than the defect. SQUAT PROTECTION IS NOT WEAKENED AND NEEDS ITS OWN ROW: a live owner whose recorded birth MATCHES still refuses the bind. NO INT STAGE, AND THAT IS A DECISION RATHER THAN A GAP (doyle, 2026-08-03): the field shape is covered at UNIT level because every fact this requirement rests on is reachable there -- the custody tri-state exhaustively, the remedy RUN from the refusal state, and both unknown-arm defaults -- while the arm that cannot be produced portably (a live pid whose birth is unreadable) is not reachable through an int seam either, since no single platform manufactures it. The int seams that would carry a field shape already run green beside these rows (`dummy_harness_e2e`'s bind/attach pair, `resume_custody_aba`), so an int stage here would re-run a topology that is exercised rather than test anything the unit rows do not. THE FIELD SPECIMEN IS REPRODUCED, NOT LOADED: the fixtures rebuild the specimen's construction (a live pid whose recorded birth is provably not its own, `pid_role` binder) and deliberately do not read ~/spt-specimens -- a test depending on a path outside the repo that no runner has would pass on one box and be silently skipped or red everywhere else. The specimen proves the hazard was field-real; the fixture is reproducible anywhere; coupling them costs the second job. Gate: impl -- the paired custody check at the bind conflict site, one snapshot, Unproven-allows; unit -- a recycled owner pid (live impostor pid whose recorded birth is provably not its own) no longer refuses the bind (RED on shipped code), a same-birth live owner still refuses (the control that keeps squat protection honest), and a `pid_role = Relay` recycled record is fixed too (the row that proves the role gate is not what carries this).
2026-09-06T13:09:02.0746565Z - Required stages: impl, unit
2026-09-06T13:09:02.0746718Z 
2026-09-06T13:09:02.0746833Z ### REQ-HAZARD-STOP-RESPAWN-CONVOY
2026-09-06T13:09:02.0748278Z - Title: KNOWN-HAZARDS 7.52: an operator stop outranks every implicit ensure — no convenience path resurrects what the operator just killed. The hazard-conformance twin of REQ-ENSURE-DAEMON-STOP-INHIBIT: its int-stage convoy rig IS this hazard's required test (stop under api-call storm -> down + zero respawns + honest refusal; start clears; one-daemon race). Registered separately per CLAUDE.md rule 4 — evidence may tag the same rig.
2026-09-06T13:09:02.0750778Z - Required stages: doc, int
2026-09-06T13:09:02.0750988Z 
2026-09-06T13:09:02.0751136Z ### REQ-STREAM-LIFETIME-CLASS
2026-09-06T13:09:02.0771355Z - Title: RESCOPED at W2 activation 2026-07-22 (doyle verify-first, C3-retirement precedent): the CLEAN-CASE chain this REQ was minted for is ALREADY SHIPPED under REQ-STREAM-LEASE-CLASSES / ADR-0040 decision 6, verified against @7c0f12d — StreamLifetime is opener-declared and on the wire, serde-default Durable so N-1 openers keep exact today-semantics (msg.rs:810-836 + the additive-wire unit @msg.rs:1408); rc attach/view is the SOLE ConnectionBound opener (attach.rs:667-677); the broker binds the class at open (broker.rs:5689) and nethost carries it per StreamEntry (nethost.rs:1059-1076); the conn-exit sweep FINs each ConnectionBound stream toward its target and terminal-retires the local row while Durable rows never enter it (broker.rs:4118-4123/4227-4235/4429-4446); the target's serve_attach EOF arm runs detach_session_gen(serve_gen) so controller/viewer stamps clear on exactly one generation (attach.rs:580-597); the late-close gen guard (broker.rs:2179-2192, unit @8416) and converge_perch_stamps close the race; int coverage rides endpoint_lifecycle.rs:241. Building that again would re-implement shipped code — the C3 lesson. WHAT REMAINS, and what this REQ now owns: RESTART-REPLAY RE-ESTABLISHMENT. ADR-0038 dispatch replay re-serves every retained opener Attach across a dispatcher generation, and a ConnectionBound opener REPLAYING is a class contradiction — the conn that declared the class cannot exist after the restart that killed it, so re-serving resurrects a seat whose declared lifetime already ended. FIX: the dispatcher's restart-durable classification (ADR-0038 decision 2, dispatch.rs first_line) gains lifetime, and the replay filter TERMINAL-RETIRES ConnectionBound openers instead of re-serving them. Deliberately PRECISE, not ADR-0038's rejected clear-table-on-restart: Durable/brain-swap semantics are untouched and the brain_swap / daemon_refresh / redispatch D1+D1b families staying green IS the proof. The filter runs UPSTREAM of the W1 idempotence key (endpoint/session, by, conn, gen, from_seq) — a filtered opener never reaches serve, so the two mechanisms compose rather than overlap. Gate: impl — lifetime on the replay classification + the terminal-retire filter; unit — the filter predicate (ConnectionBound retires / Durable replays / unclassifiable stays terminal as today); int — RED-first: kill the rc opener raw, restart the broker, pre-fix the seat and CONTROLLED stamps re-establish via replay, post-fix they stay clear, with the D-legs green alongside. Kin REQ-STREAM-LEASE-CLASSES (the shipped clean case), REQ-ATTACH-IDEMPOTENT-REPLAY (downstream), REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT (the live no-FIN sibling, same wave). RESCOPE 2 — MEASURED 2026-07-22 (todlando, Leg A rig `restart_replay_lifetime.rs`, GREEN with its sibling probe passing): the FIX ABOVE IS NOT OWED. The premise was pre-registered as falsifiable by doyle BEFORE the rig was built, and the rig falsified it: in the residual ordering (dispatcher generation dies FIRST so no serve worker and no Served report, THEN the rc opener dies RAW with no detach frame) a fresh generation does NOT resurrect the seat — stamps stay clear and no DISPATCH line touches the dead opener's row. The population is closed BY COMPOSITION: the conn-exit sweep fires for this kill shape (`STREAM_CONNBOUND_RETIRE:<id>: opener conn exited — FIN + terminal retire`), the ConnectionBound opener row leaves the table entirely, its peer row carries the FIN as finished=true, and the worker's family-aware finished_row_is_terminal(Attach,true) retires it UNSERVED. TWO PRE-REGISTERED SUB-QUESTIONS ANSWERED, and the first is the non-obvious finding: (1) `retire` does NOT exclude from the CLAIM condition on this path — the dead opener's row is STILL in the dispatcher's claimable enumeration, merely finished=true, so what stops the replay sits DOWNSTREAM of the claim (retire-on-sight in the worker), which is ADR-0038 decision 1's `excluded by lifecycle state` arm and NOT its `removed from the enumeration` arm; (2) the sweep DID run for this kill shape. Consequently NO wire field (already settled independently: the re-served row registers Durable, class-blind by design at nethost.rs:1028-1041 / 1763-1771, so a lifetime field on the opener reply would report Durable for exactly the population a filter exists to catch) and NO broker-side enumeration filter are built. Stages drop to int ONLY — impl/unit are deactivated rather than pre-failed (the `activate, don't pre-fail` rule; the C2/C3 verify-first precedent), and the int evidence is the standing regression guard the property never had. SCOPE LIMIT STATED, NOT IMPLIED — and TIGHTENED 2026-07-22 (doyle rider, todlando self-flagged): the measurement rides the IN-PROCESS DUPLEX, which is the honest word and is tighter than the `loopback` this text first said. net_dial_loopback / open_loopback_pair yield a RecvHalf::Loopback duplex with NO QUIC ANYWHERE and NO idle timeout — a different transport leaf from RecvHalf::Quic; BindScope::Loopback plus a real net_dial WOULD be real QUIC over 127.0.0.1, and the two senses of `loopback` in this repo are NOT the same thing (the naming collision is itself an ADR-0040 amendment item, so future rigs pick their venue deliberately). Consequently the cross-node `conn.closed()` arm (nethost.rs:1000-1021) is a different mechanism reaching the same outcome and is NOT exercised, and a real-QUIC transport death sans FIN remains Leg B's instrument-first residual under REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT.
2026-09-06T13:09:02.0785669Z - Required stages: int
2026-09-06T13:09:02.0785804Z 
2026-09-06T13:09:02.0785912Z ### REQ-STAMP-CONVERGENCE-ORDER
2026-09-06T13:09:02.0788620Z - Title: RETIRED at W2 verify-first (doyle 2026-07-22): the C3 poll-vs-reap ordering race (hertz emphasys RCA 2026-07-16, P1) was ALREADY CLOSED by REQ-CONTROL-STAMP-CONVERGENCE (ADR-0041 decision 4, REGISTRY-LIFECYCLE W2 build todlando 2026-07-17 — one day after the RCA). Code-verified chain: the exit-waiter reap (broker.rs stamp_reaped) bumps a per-endpoint StampSlot generation UNDER the stamp-write serial then clears (the comment names the emphasys C3 window); the KIND_SESSIONS handler snapshots stamp_gen under the log lock; converge_perch_stamps validates snap_gen under the same write serial and REFUSES stale (STAMP_STALE_SKIP) — a pre-reap snapshot can never relatch controlled=true. Full stage coverage rides that REQ: unit stale_snapshot_stamp_write_refused_after_reap_generation_bump + int endpoint_lifecycle.rs:488. Kept as a stable pointer; no build owed.
2026-09-06T13:09:02.0791067Z - Required stages: 
2026-09-06T13:09:02.0791192Z 
2026-09-06T13:09:02.0791292Z ### REQ-ATTACH-RESIZE-REPAINT
2026-09-06T13:09:02.0797964Z - Title: RESCOPED 2026-07-22 by ADR-0047 Amendment 2 (doyle-ratified): this REQ no longer asserts a defect. Its original claim — that the attach-stream resize never arms the presentation barrier, and that this is v0.39.4 field bug 3's root — is RETRACTED; the W3 bisect falsified it three ways (static trace send_attach_resize -> serve_attach Resize arm -> brain.resize -> KIND_RESIZE -> dispatch_resize -> begin_resize_serialized; the synthetic sibling's pass; the broker's dynamic trace attach -> RESIZE_DISPATCH -> SYNC_PUSH{controller=true} with no RESIZE_SYNC_DROP) and then pinned the real root in the rc CLIENT's console mode (REQ-RC-NEWLINE-PRESENTATION-TRUTH / KNOWN-HAZARDS 7.56). The ADR-0047 Amendment 1 DECISION (geometry change = one transaction, one exit shape, any entry) stands on its own merits and is not disturbed. WHAT THIS REQ NOW OWNS — the captured-shape regression apparatus that the bisect proved worth keeping: hertz's ENLYZEAM production capture + its integrity pins (*.log -text, the fixture README sha256 table, per-record length refusal in both the player and the reader); the capture-player replay vehicle; and the probe ladder that makes any verdict from it mean something — vehicle transparency per CELL (ConPTY re-renders, so byte equality is unsatisfiable by construction), the pre-state soundness self-check (narrow-captured bytes into a wide authority is sound only if absolutely addressed), the capability probe re-run at every shape revision with its own precondition asserted, and the no-vehicle control (grid+resize+repaint over the same bytes = 0 divergent rows, the standing proof the seam is clean). Oracle clauses are binding for this rig class and are written out in Amendment 2. Gate: unit — the vehicle-transparency predicate + its non-vacuity sibling + the record-integrity refusals; int — the client-model rig pair (rc-fixed vehicle = clean, rc vehicle = reproduces the field scrap) plus the no-vehicle control. NO impl stage: there is no broker-side implementation owed, and pretending otherwise would point an activated stage at evidence that does not exist. Kin: REQ-RC-NEWLINE-PRESENTATION-TRUTH (the actual fix), REQ-RESIZE-INPUT-MODE-INTEGRITY (7.55, untouched — its capture has no replay vehicle in its path).
2026-09-06T13:09:02.0804348Z - Required stages: unit, int
2026-09-06T13:09:02.0804533Z 
2026-09-06T13:09:02.0804668Z ### REQ-RESIZE-INPUT-MODE-INTEGRITY
2026-09-06T13:09:02.0813730Z - Title: RESCOPED 2026-07-22 by ADR-0047 Amendment 2 + KNOWN-HAZARDS 7.55 (doyle-ratified), on the measurement rather than on the supposition. FIELD EVIDENCE IS REAL: hertz's second ENLYZEAM capture shows the operator's keystrokes as isolated one-byte OUTPUT records in the child->broker RAW DRAIN — server-side, upstream of any client console, so independent of the 7.56 presentation root that explains the other half of the /c/config line. LOCAL SEAM MEASURES CLEAN: a probe child under a real ConPTY, clamped raw at startup and resized 24x80->60x131 live, reports an IDENTICAL input mode word at four sample points (boot / before-resize / after-resize / before-write, all in=0x000001f0 echo=0 line=0), and typed bytes do not come back as output; a seeded capability probe moves BOTH observables, so the absence is earned, not vacuous. Amendment 1's addendum supposed the resize path re-enables console echo — on this box and portable_pty/ConPTY version it does NOT. TRIGGER UNPINNED. OPEN CANDIDATE (hertz field question, deliberately NOT built against): the pseudoconsole BOOTS with ENABLE_ECHO_INPUT/ENABLE_LINE_INPUT ON (in=0x000001f7 measured) and the CHILD clamps them off, so any UNCLAMPED WINDOW (startup, or a re-clamp after a TUI state transition) echoes by default with no seam re-enabling anything — reframing the question from who turns echo back on to whether there is a window where nobody has turned it off yet. NO impl STAGE, for a structural reason and not a scheduling one: the hosted child's console handles belong to the pseudoconsole it is attached to and the daemon holds only the master end, so the daemon can neither OBSERVE nor SET those modes — instrumenting spt-term's resize seam would probe a console that is not the one in question, and a server-side clamp is equally impossible. Shipping instrumentation that measures the wrong console is manufactured confidence (doyle, declined explicitly); the RIG is the instrument, and the probe-child pattern ships as a debug tool if the field ever needs live diagnosis. The restore-placement question (spt-term seam vs broker write_input) is MOOT, not answered: there is nothing to restore when the mode is preserved. WHAT THIS REQ OWNS: unit — the probe protocol contract (the verdict travels in the stdout protocol line because under a ConPTY the child's stderr is interleaved into the same re-rendered stream and arrives shredded, so a refused SetConsoleMode could otherwise read as a measurement); int — the four-sample measurement, the typed-bytes-do-not-echo symptom leg (which survives being wrong about the mechanism), and the seeded capability probe. Rig-craft pinned in the rig and transferable: a probe that never clamps raw sits at the very default the seam is suspected of restoring and cannot tell reset from never-changed; and ConPTY's post-resize REPAINT re-emits earlier output, impersonating a fresh reply to any rig that matches loosely. Kin REQ-RC-NEWLINE-PRESENTATION-TRUTH (7.56, the presentation half, FIXED) and REQ-ATTACH-RESIZE-REPAINT (7.54, rescoped the same honest way).
2026-09-06T13:09:02.0821737Z - Required stages: doc, unit, int
2026-09-06T13:09:02.0821948Z 
2026-09-06T13:09:02.0822052Z ### REQ-NOTIF-DRAIN-ROW-VALIDITY
2026-09-06T13:09:02.0827967Z - Title: A spooled notify envelope is validated against its notif row at DELIVERY time — a copy outliving its row must not deliver. (ADR-0046 Amendment 1 + KNOWN-HAZARDS 7.53; operator field regression from perri's node 2026-07-22, doyle root-caused same day — DAEMON-LIFECYCLE W1 RIDER.) TODAY: quiet delivery (REQ-NOTIF-QUIET-DELIVERY) makes every surface an active_only SPOOL write per endpoint, and every row-lifecycle mechanism (apply-seam dismissal REQ-NOTIF-SEAM-DISMISS, coalesce supersession, TTL, the one-shot migration) touches ROWS only — so a copy spooled while the endpoint was busy/offline is a detached snapshot no dismissal can recall, delivering 'update available' on an already-updated node at the next drain, once per qualifying surface event (perri: twice; doyle's own session: four stale 0.39.x drains post-upgrade). FIX: at the safe-point drain choke point (api poll deferred presentation, spt/src/api/delivery.rs cmd_poll), a notify-kind envelope delivers ONLY if its notif_id resolves to a live UNDISMISSED row in the local notif store; dismissed/superseded/TTL-expired/unknown -> dropped silently; N copies of one notif_id in a drain dedupe to ONE delivery. Non-notify spool content is untouched (ordinary messages are not revocable and MUST deliver). Legacy pre-0.40.0 plain-message copies carry no notif_id, cannot be validated, and self-clear by their one drain — no machinery. Rejected: recall-on-dismiss spool sweeps (race the drain, span every perch, miss in-flight copies); live-only notify delivery (the spool IS quiet delivery's safe-point mechanism). Gate: impl — the validity gate + dedupe at cmd_poll's deferred presentation; unit — the gate predicate cells (live row delivers / dismissed drops / unknown drops / dedupe collapses / non-notify passes untouched); int — the perri rig RED-first: surface an update notif to a non-draining endpoint (copy spools), dismiss the row via the apply seam, drain -> NOTHING delivers; control leg: undismissed row's copy delivers exactly once.
2026-09-06T13:09:02.0833284Z - Required stages: impl, unit, int
2026-09-06T13:09:02.0833467Z 
2026-09-06T13:09:02.0833590Z ### REQ-NOTIF-UPDATE-ROW-VERSION-RETIRE
2026-09-06T13:09:02.0842881Z - Title: An update-available notif row minted by a node running PRE-0.40.0 spt is retired on the version the running node has ALREADY reached — because the keyed catch-up dismissal is structurally blind to it. (DAEMON-LIFECYCLE W2 RIDER, operator-ordered; doyle root-caused end to end 2026-07-22 on the live box.) FIELD CHAIN, verified: GRAVITY-NVDA-PC (f15d837b, BIGNET) runs pre-0.40.0 spt, whose legacy producer mints the update notice SUBNET-scoped with NO coalesce key (the scoped+keyed producer shipped in 0.40.0). The row replicated fleet-wide. The modern catch-up dismissal (REQ-NOTIF-SEAM-DISMISS, pump/update.rs dismiss_staged_notif_if_caught_up) dismisses ONLY by coalesce key, so a keyless row can never be retired by it: a FULLY-UPDATED node holds a live 'v0.41.0 available' row forever, surfacing once per endpoint at every boundary (observed: todlando ~19:54 + Librarian/Athenaeum-Library; store showed seen=2, undismissed, the only undismissed update row in the whole history). FIX: a version-grounded retirement sweep at the EXISTING catch-up site, same per-tick per-subnet cadence, running ALONGSIDE the key path (which stays PRIMARY — belt-and-braces, not a replacement): dismiss any UNDISMISSED row whose from_id == 'spt-update' AND whose body advertises a version that PARSES AND is <= the running image's own env!(CARGO_PKG_VERSION) (the same running-image self-truth the existing out-of-band leg reads). CONSERVATISM INHERITED VERBATIM from version_ge: an unparseable advertised version NEVER dismisses (the ancient 'update-available 5' counter-shape stays untouched), and advertised > running STAYS LIVE (a genuinely pending notice must survive the sweep). MUST NOT KEY ON kind ALONE: kind=consent is SHARED with the grants act-gate asks (grants.rs mints consent-kind through the plain front door with from_id = the asking agent), so from_id == 'spt-update' PLUS the body version-parse is the discriminator. The relevance predicate stays DAEMON-side (ADR-0046: the notif primitive evaluates no relevance predicate) — the store keeps only its existing undismissed()/dismiss() primitives. KNOWN LIMIT, stated not hidden: the ORIGIN node's one-time cross-node wake at mint is a plain live send from old code and is not preventable from our side; retiring/updating the legacy node is the operator's lever. This fix kills the row's AFTERLIFE (undismissable + boundary-resurfaces at every endpoint forever), which is the repeating harm. Gate: impl — the sweep + the pure advertised-version extractor at the catch-up site; unit — the extractor cells (dotted token found / v-prefixed / counter-shape and garbage refused) and the retire predicate; int — RED-first four legs: (1) legacy-shaped row (subnet scope, NULL key, from_id spt-update, advertised <= running) SURVIVES pre-fix ticks and is DISMISSED post-fix; (2) sibling advertising > running stays live; (3) sibling with a garbage/counter-shape body stays live; (4) regression: a modern Node-scoped KEYED row still dismisses via the key path; plus an asserted (not assumed) replication leg — a sweep-minted dismissal merges as dismissed through notifsync, since the fleet-wide kill is the point.
2026-09-06T13:09:02.0851170Z - Required stages: impl, unit, int
2026-09-06T13:09:02.0851318Z 
2026-09-06T13:09:02.0851429Z ### REQ-ATTACH-SEED-REQUEST-DOUBLE-SERVE
2026-09-06T13:09:02.0857638Z - Title: A dispatcher-SEEDED attach Request is honored ONCE — the wire replay's copy of that same Request must not re-establish it. (Seed: DAEMON-LIFECYCLE W1 build, answering ADR-0047 decision 3's banked open question; origin CODE-PROVEN, not inferred.) THE ORIGIN: serve_attach (spt-daemon/src/attach.rs) processes the dispatcher-pinned `seed_request` through handle_attach_request BEFORE its event loop (ADR-0038 B, the durable-opener seed — it exists because a ROLLED ring can no longer replay the Request). It then subscribes the stream from the floor and enters the loop, whose `AttachRecord::Request` arm calls handle_attach_request UNCONDITIONALLY — there is NO `!attached` guard, and NO record that the seed already established this exact Request. When the ring has NOT rolled, the wire replay still carries that Request, so the SAME record is handled TWICE on the SAME conn, milliseconds apart, with identical (session_id, from_seq, intent, gen, identity) — exactly hertz's v0.39.4 bug-4 capture (conn1011, 15ms apart, post-refresh replay). The two arms are the same function on the same seed record; the seed is pinned by the dispatcher on the post-cycle replay, which is the field trigger. SIBLINGS to check at triage: any other serve family that seeds an opener record AND re-reads it from the replay (xfer/rest seeds), and whether a re-established seat should also re-run the access check. FIX SHAPE (not built, W1 deliberately shipped the broker-side idempotence instead): the serve worker knows it already honored a Request — a guard on the loop's Request arm (already attached + same record = ignore), which fixes the ORIGIN rather than absorbing it at the broker. The broker idempotence stays regardless: it closes the symptom for EVERY origin, including origins not yet found. A serve-path int rig replaying the REAL double-serve shape belongs to this REQ, not to W1 (doyle ruling 2026-07-22: do not simulate a behavior we have not explained). Kin REQ-ATTACH-IDEMPOTENT-REPLAY (the broker-side absorber), REQ-STREAM-OPENER-DURABLE (why the seed exists), ADR-0038 B.
2026-09-06T13:09:02.0863270Z - Required stages: 
2026-09-06T13:09:02.0863401Z 
2026-09-06T13:09:02.0863505Z ### REQ-RC-NEWLINE-PRESENTATION-TRUTH
2026-09-06T13:09:02.0867336Z - Title: W3 (v0.39.4 field bug 3 ROOT, pinned 2026-07-22): the rc client presents relayed bytes exactly as the emitter addressed them — KNOWN-HAZARDS 7.56. rc's with_vt_output enables ENABLE_PROCESSED_OUTPUT without DISABLE_NEWLINE_AUTO_RETURN (DNAR: zero grep hits tree-wide), so the operator's console translates relayed bare LF into CR+LF — a column reset the emitter never asked for; the /c/config field scraps are capture record t=1784714152501's bare-LF index presented through that mode. Relay byte-clean end to end (why both isolated rigs read NOT-REPRODUCED and the fresh viewer read clean — the synthesized repaint is CUP-absolute). FIX: DNAR joins the with_vt_output composition whenever processed/VT output is enabled; prior-mode capture/restore unchanged; Unix needs nothing (raw mode already clears OPOST/ONLCR). Regression: the W3 captured-bytes rig PAIR (vehicle console without DNAR = field-client model = RED pre-fix; with DNAR = GREEN post-fix) + the minimal LF column-preservation probe through an rc-mode console. Amends REQ-RC-WIN-VT-OUTPUT (#12); kin KH 7.55 + the W3 vehicle-fidelity clause (three console-mode-seam manifestations, one day). Attribution CLOSED when hertz confirms fixed-rc clean on the field session; no debug channel needed.
2026-09-06T13:09:02.0870929Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0871100Z 
2026-09-06T13:09:02.0871210Z ### REQ-TERM-ECHO-CLAMP-WINDOW
2026-09-06T13:09:02.0877214Z - Title: SEED (DAEMON-LIFECYCLE W3 rideout, field-grounded 2026-07-22): the hosted-TUI echo CLAMP WINDOW — a Windows pseudoconsole boots with echo/line input ON (in=0x1f7, measured by the 7.55 instrument @0f74bba) and it is the hosted CHILD that clamps them, so any window in which the TUI has not yet (re-)clamped echoes typed bytes into PTY output SERVER-SIDE with no seam re-enabling anything. FIELD GROUNDING (hertz capture-2 byte timeline, output-only taps): echo onset is NOT resize-instant — first echoed key lands 6003ms after the first resize-associated repaint burst, IMMEDIATELY after a 3225-byte TUI-reinitialization-shaped absolute repaint (no alt-screen/mode CSI anywhere — cursor hide/show + HOME/absolute repaints only); echo CEASES mid-input (isolated c/o/n/f/i, NO g) directly after a 535-byte TUI diff — consistent with a late clamp landing (a WinAPI mode call is invisible to a byte tap). The resize seam itself is measured MODE-PRESERVING (REQ-RESIZE-INPUT-MODE-INTEGRITY rig, four sample points). OPEN AT TRIAGE: (1) whether the clamp is the TUI's own SetConsoleMode or portable-pty/ConPTY-internal; (2) whether spt-core CAN mitigate at all — the daemon structurally cannot read or set the child console's input modes from the master side (platform fact, 7.55), so candidate mitigations are indirect (e.g. hold/queue injected input until first child output after a reinit-shaped burst — evaluate honestly, may be worse than the disease) and the honest outcome may be an OUT-OF-OUR-CODE finding filed upstream (hosted-TUI reinit behavior + Windows boot default); (3) whether the window also explains historical spt-hosted-only echo reports (non-spt sessions bypass the nested ConPTY — KH 7.55 note). Kin: KH 7.55 (hazard + instrument), KH 7.56 / the DNAR class (console boot defaults and clamp ownership = one seam family), REQ-ATTACH-SEED-REQUEST-DOUBLE-SERVE (the rideout-seed precedent). Instrument + rig to reuse: the 7.55 probe child (protocol-line verdict, raw-first precondition, seeded-echo capability probe).
2026-09-06T13:09:02.0882669Z - Required stages: 
2026-09-06T13:09:02.0882794Z 
2026-09-06T13:09:02.0882894Z ### REQ-ACTIVITY-LINK-PUSH
2026-09-06T13:09:02.0886620Z - Title: An owned Shell observes its owner's busy/idle transitions as an activity frame on the EXISTING shell-link event stream (ADR-0048 decisions 1-3; perri contract-lock 2026-07-24). Link-scoped (owner implied by the link token — no new verb, no addressing surface). Drive-class semantics: ephemeral, latest-wins, current-state-carrying; redundant same-state resend is a harmless no-op (consumer derives edges); NEVER spooled or replayed (stale transitions are actively wrong). Both directions (busy->idle, idle->busy). Current state emitted on EVERY link establishment and re-link — restart resync is free. Latency promise = BOUNDED OBSERVATION: a frame per transition, sub-second class, never hard-real-time; each frame carries the TRANSITION TIMESTAMP (when the sentinel flipped, not when the frame was emitted) so edge-anchored consumers self-correct emission latency. Frame name/attrs publish in the shell frame vocabulary doc (REQ-SHELL-FRAME-VOCAB lineage) — doc stage rides there. Gate: int — a live shell link sees busy->idle and idle->busy frames with truthful transition timestamps, a re-link re-emits current state, and a transition during link-down is represented by the re-link emit (current state), never a replayed backlog.
2026-09-06T13:09:02.0890279Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0890442Z 
2026-09-06T13:09:02.0890547Z ### REQ-ACTIVITY-INFO-PULL
2026-09-06T13:09:02.0891642Z - Title: The endpoint's current activity state (busy|idle) is readable via `spt api endpoint-info` — a point-in-time read of the perch idle sentinel, for consumers that need a check rather than a stream (ADR-0048 decision 1, pull avenue; operator-ruled 2026-07-24). Additive key, N-1-safe per the additive-evolution posture.
2026-09-06T13:09:02.0892571Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.0892730Z 
2026-09-06T13:09:02.0892829Z ### REQ-ATTACH-AWAY-ALERTS
2026-09-06T13:09:02.0898263Z - Title: AN ENDPOINT LEFT UNATTACHED FOR FIVE MINUTES IS TOLD SO, ONCE, ON THE HOOK CHANNEL, AND TOLD AGAIN WHEN SOMEONE COMES BACK -- BUT ONLY IF IT WAS TOLD THE FIRST TIME (releases#45, rides REQ-ATTACH-LINK-PUSH). THE AWAY TEXT IS THE TICKET'S, VERBATIM, AND IS NOT OURS TO REWORD: it tells the agent to proceed but not to assume the user can see its output, to WITHHOLD user-aimed information until a controller or viewer attaches, that it will be notified when that happens, and -- second paragraph -- to share such information with viewed-and-relevant same-node endpoints it is actively collaborating with while avoiding unwarranted comms. It lives as ONE CONSTANT so the wording has a single home and its verbatim-ness is auditable at a glance rather than reconstructed from a formatter. IT IS SENT active_only, WHICH IS THE POINT AND NOT A DETAIL: active_only is the spool-only hook channel with NO live TCP and NO wake, so a message about nobody watching cannot itself be the thing that wakes an agent -- it arrives at the next turn boundary, which is the only moment the agent can act on it anyway. FIRE-ONCE LIVES IN THE CONDITION, NEVER IN A NOW-SIGNAL SEEN-SET (doyle, 2026-08-30): the away/return pair is delta-disciplined by construction -- away fires on the unattached edge at five minutes and latches, return fires on the re-attach edge and clears the latch -- so borrowing a seen-set would put the same fact under two disciplines and make which one is authoritative a question. THE RETURN NOTICE FIRES ONLY IF AWAY FIRED: an agent that was never told nobody was watching must not be told someone came back, because the second message is meaningless without the first. Its wording is NOT fixed by the ticket, which asks only for a short reciprocal message that may assume the agent remembers the away context -- so it is authored here, short, and deliberately does not restate what the away message said. RE-ATTACHMENT IS ANY ATTACHMENT: a controller or a viewer, from any node, ends the away condition, because the away message's own promise is about a controller OR viewer attaching and a notice that honoured a narrower rule than the promise would be a broken promise.
2026-09-06T13:09:02.0903971Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.0904181Z 
2026-09-06T13:09:02.0904272Z ### REQ-ATTACH-LINK-PUSH
2026-09-06T13:09:02.0910302Z - Title: AN OWNED SHELL OBSERVES ITS OWNER'S ATTACHMENT STATE AS AN ATTACHMENT FRAME ON THE EXISTING SHELL-LINK EVENT STREAM (releases#44). The SIBLING of REQ-ACTIVITY-LINK-PUSH in every structural respect, and deliberately so: link-scoped (the owner is implied by the link token, no new verb and no addressing surface), drive-class semantics -- ephemeral, latest-wins, CURRENT-STATE-CARRYING, so a redundant same-state resend is a harmless no-op the consumer derives edges from -- NEVER spooled and NEVER replayed, because a stale attachment is actively wrong rather than merely old. Current state is emitted on EVERY link establishment and re-link, which is what makes restart resync free. Bounded observation, sub-second class, never hard-real-time. IT GETS ITS OWN DriveHub SLOT CLASS: latest-wins is only correct WITHIN a class that supersedes itself, so an attachment push must never be able to eat an undelivered drive command or an activity frame, nor they it. WHAT THE FRAME CARRIES: whether a controller is attached and from which NODE, how many read-only viewers are attached and from which NODES, and which node CHANGED on this edge. THE VIEWER NODE LIST IS RETAINED IN-MEMORY ON THE BROKER'S ViewerSink FOR FRAME COMPOSITION ONLY, and that bound is part of the requirement rather than an implementation note: THE DURABLE CONTROL TRIPLE (driven_by / controlled / viewer_count) IS UNCHANGED, and durable viewer identity, if it is ever wanted, is ITS OWN REQUEST (kin: the register's IR-72 custody-surface gap -- noted in a comment, never built toward). The measurement that justifies retaining it at all: the viewer's origin node ALREADY REACHES the broker on the attach path (attach.rs attach_as_viewer_snap -> brain.rs subscribe_with `by`) and is DROPPED on the floor by a ViewerSink that keeps only tx/send/evicted/writer -- reachable-and-discarded on the correct write path, which is materially different from a source that was never built in the arm at all (releases#11b) and is why this is a retention rather than a board request. Gate: int -- a live shell link sees an attachment frame on controller attach and detach and on viewer attach and detach, naming the changed node; a re-link re-emits current state; and an attachment change during link-down is represented by the re-link's current-state emit, never by a replayed backlog.
2026-09-06T13:09:02.0916626Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0916793Z 
2026-09-06T13:09:02.0916892Z ### REQ-ACTIVITY-LIST-JSON
2026-09-06T13:09:02.0917990Z - Title: `spt endpoint list --json` carries a per-endpoint `activity` (busy|idle) key, for consumers surveying the idle/busy state of many endpoints at once (ADR-0048 decision 1, roster pull avenue; operator addition 2026-07-24). Additive key, N-1-safe; kin the flynn 2026-07-06 last-active/description/adapter list-enrichment seed (same additive posture, may ride together).
2026-09-06T13:09:02.0919125Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.0919279Z 
2026-09-06T13:09:02.0919383Z ### REQ-DIGEST-SEAL-ON-IDLE
2026-09-06T13:09:02.0922814Z - Title: A digest turn's records gain their stable `seq` at the endpoint's IDLE TRANSITION — the adapter-reported, already-authoritative turn-finished signal — not at next-user-input arrival (which is demoted to a harmless no-op fallback seal). Kills the mint-then-idle deadlock (liam/spt-progress-tool W16: a finished-but-idle turn stayed partial/seq-less, so a seq-keyed scanner could never see the owner's latest turn until the owner was prompted again; interim drive-one-more-command workaround retires at ship). Sealing is IDEMPOTENT and SEQ-STABLE: it assigns seq to what the log holds at idle; a late-flushing straggler record folds into the sealed turn WITHOUT changing the assigned seq. (ADR-0048 decision 4; rejected: extractor-observed completion = banned heuristic inference class; provisional seqs = scanner bug factory.) Gate: int — the liam field shape RED-first: finish a turn, report idle, do NOT prompt again — the turn is sealed with a stable seq and a scanner keyed on seq sees it; fallback leg: next-input on an already-sealed turn re-seals nothing; straggler leg: a post-idle log record folds in, seq unchanged.
2026-09-06T13:09:02.0926073Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0926236Z 
2026-09-06T13:09:02.0926341Z ### REQ-DIGEST-CROSS-NODE-PULL
2026-09-06T13:09:02.0929312Z - Title: The digest SNAPSHOT PULL crosses nodes: `spt endpoint digest <[subnet:]id@node>` (including `--after <seq>` incremental polling) resolves and serves under the EXISTING address gate — the CROSS_NODE_M4 qualified-address refusal lifts for the pull verb only. The structured-delta stream stays node-local until a real cross-node subscriber exists (ADR-0048 decision 5, defer-until-consumer; operator-ruled 2026-07-24). Access policy is NOT new — fetch was ratified address-gated long ago; this is routing. Composition note: REQ-DIGEST-SEAL-ON-IDLE's stable seqs are what make remote --after polling trustworthy. Forcing customer: spt-mobile 'view live digest of ANY endpoint' (host proxies transparently once the refusal lifts). Gate: int — two-node rig: a qualified digest pull from the non-hosting node returns the hosting node's buffer; --after returns only rows past the cursor; the address gate still refuses a non-resolvable caller.
2026-09-06T13:09:02.0931883Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0932045Z 
2026-09-06T13:09:02.0932158Z ### REQ-HAZARD-SHELL-STALE-ONLINE
2026-09-06T13:09:02.0966741Z - Title: A shell instance's ONLINE-ness is DERIVED (recorded status AND its recorded `shell.pid` not provably dead), never the recorded `status` field alone — an abruptly-dead binary (force-kill, crash, OOM: no link-break, so `close_shell`'s offline flip never runs) must not read online forever. The shell-side twin of REQ-HAZARD-DAEMON-HOSTED-LIVENESS, which gave AGENT perches exactly this resolver and which shells never got. PROVABLY DEAD is the narrow discriminant: `shell.pid` present AND parses non-zero AND `!is_process_alive` — pid absent, unparseable, or 0 (a broker-hosted spawn whose backend exposed no pid records 0) reads ALIVE, the same interim-parity/fail-toward-alive stance `liveness.rs` already holds, so a pid-less backend is NEVER falsely declared dead. Recycled-pid caveat, accepted at mint: a reused pid reads alive, so the heal is missed, never mis-fired — the failure direction is 'stays stale', never 'kills a live instance'. SITE CLASSIFICATION IS PART OF THE REQUIREMENT (authoritative cfg(test)-excluded census at mint = 8 status reads, 3 classes — do NOT blanket-swap the predicate): (a) DERIVED — relink's already-online refusal (the gate that made recovery impossible), the `shell cmd` wake-if-offline arm (which silently spooled to a corpse), the drive drop-if-offline branch, `shelldisc::discover` (the single source of BOTH `shell list` renders, text + --json), and the activity fan-out's online filter; (b) RAW status, deliberately — `bind_shell_by_token`/`close_shell` (the WRITERS) and `cascade_owner_edge`'s suspend-close arm, where routing through the resolver would SKIP the close that is itself the cleanup, removing a heal path; (c) RAW recorded status AND the corpse-boot discriminant — the `shellwake::reconcile_once` watcher-eligibility read. AMENDED BAROMETER W2 (doyle, releases#78 comment 5156810267): this class's original text — 'RAW status, pending an operator ruling', with no spontaneous relaunch of anything — IS STRUCK, because it rested on a fact leg (a) deliberately ends. What actually held a force-killed instance out of the eligible set was the STALE `online` record itself; healing that record to the truth removes the accidental protection, so the guard had to move to a rule that STATES it. Eligibility is now recorded `offline` AND (no corpse, or the corpse's launch PREDATES THE BOOT INSTANT), reusing leg (b)'s `launch_predates_boot` and its slack constant verbatim. NO SPONTANEOUS RELAUNCH OF SAME-BOOT DEATHS: flynn's mid-deploy forcing case (an operator kills the process precisely to free the exe for overwrite — shared install dir ⇒ 'kill the process' is a routine deploy step) is preserved BY CONSTRUCTION, since a force-kill during steady state leaves a corpse launched AFTER boot, which the predicate can never accept; for those, recovery stays demand-driven (an unblocked `relink`, or a `shell cmd` that wakes). Boot-restoration of PRE-boot casualties is enabled, per #78 leg (b)'s operator-greenlit scope and this ruling (#78 thread). CORPSE IS DEFINED ON THE PARKED STAMPS, never on a bare pid re-probe: a RECYCLED pid — alive, but its native start stamp mismatching the one leg (a) parked — is corpse-equivalent because our process is dead, and the parked ABSOLUTE launch stamp is what decides pre/post-boot. Without that, the heal-then-recycle race reads 'alive ⇒ no corpse' on the next tick and walks back into the forbidden shape through a side door. Deriving is still WRONG at this site and this is NOT a swap to class (a): the census stays 8 reads / 3 classes — the site reclassifies, it does not multiply, the do-NOT-blanket-swap sentence above stands, and `shell list` still tells the truth by derivation (class (a), untouched). LOCALITY SYMMETRY (field-caught by flynn's leg-2 run, 2026-07-25): 'a shell cmd that wakes' holds for BOTH the local CLI cmd and the cross-node serve — at mint the wake arm lived in the SHELL_LINK_CMD serve handler only (module docs scoped wake-if-offline to remote cmd), so a LOCAL cmd against a corpse spooled silently and nothing woke, the exact accepted-happily/drained-by-nobody shape this req exists to kill; the arm is now the shared `linkhost::wake_if_offline_persistent`, called by both, and the derived status READ inside it is the same single census site as before (the census of 8 reads/3 classes is unchanged — the read moved, it did not multiply). ROTATION CARRIES THE DURABLE CHANNEL (flynn's frame-loss field catch, 2026-07-25, both trials): a spooled command frame is MAC-stamped under the link token current at SPOOL time, and the drain is a raw destructive passthrough — the shell verifies against its CURRENT key — so a relink's token mint orphaned every pending frame: drained to the woken binary, failed verify, correctly discarded BY THE ADAPTER, lost permanently; the wake-triggering command itself was the frame the wake lost (#23 armed the wake and never answered; #24/#19 spooled after rotation and drained in order). The fix is three-layered (todlando's A1/A2 race+crash hazards addressed at mint): (1) the token mint re-stamps all pending rows old-key→new-key (idempotent, crash-partial converges — an old-stamped remainder is converted by the next rotation or drain); (2) the rotated-out token is STASHED (`link.token.retired`, never a live credential — bind resolves only the parked file) so (3) the drain paths give any race straggler a second-chance re-stamp at delivery (`restamp_for_drain`), while frames verifying under neither key pass through untouched for the adapter to refuse exactly as before. Rows are selected by MAC verification against the threaded new key, never by token-snapshot equality (the D-2 class). MOCK CONFORMANCE IS A PROPERTY OF THE RIG, not a patch (todlando A4): mock-shell MUST verify inbound frame MACs exactly as the public contract demands of a real adapter and drop failures loudly — a mock that accepts what the field rejects is a broken rig, and that exact divergence (credulous mock) is how the frame loss passed the e2e while failing in the field. The adapter-side discard behavior is CORRECT and must never be softened to paper over the spool side. Relink additionally probes LOCALLY rather than trusting a daemon sweep, so recovery holds with the daemon down. RECOVERY PRESERVES CONSUMER STATE (the property consumers actually depend on, flynn 2026-07-25): 'same canonical id, same perch' exists so that state a consumer PERSISTED IN THE PERCH survives the recovery — an adapter's repo binding, a scanner's cursor. The teardown+spawn workaround destroyed exactly that, and its worst cost was SILENT, not the rename: alchemy's tag cursor re-baselines at the digest tip, so tags written between the kill and the re-bind are never scanned — not failed-and-retried, just never seen. A loud failure gets retried; a silent one does not. Gate: int — force-kill a bound persistent instance's process, then prove (1) `shell list` reads offline, (2) `relink` succeeds instead of SHELL_ALREADY_ONLINE and the SAME canonical id + perch survive, carrying perch-persisted consumer state with them (no teardown+spawn, no id churn, no re-baselined cursor), (3) no spontaneous relaunch occurs while the instance sits dead and undriven. FIELD VERDICT — PASS END-TO-END (flynn, alchemy-0, delivered 2026-07-26; v0.43.0, counter 77): every gate leg held in the field, on a record the pre-fix code had already poisoned. (1) `shell list` read OFFLINE while the on-disk info.json still said status=online — the daemon DERIVED offline from the corpse pid (29036 absent from the process table); the record was never corrected and did not need to be. (2) relink ADMITTED, no SHELL_ALREADY_ONLINE — verbatim SHELL_RELINKED:alchemy-0 owner=flynn pid=38644 status=offline, list online thereafter. (3) identity/state integrity: same canonical id, same owner, same perch; repo binding byte-identical (token_provenance=gh-cli); armed=true preserved; no spontaneous relaunch across the ~6.5h dead window. CURSOR SCOPING, ruled at closure (doyle 2026-07-26) — the one non-byte-identical field: the tag cursor moved 206158430541→210453397553 (gen:seq 48:333→49:49) because the recovery crossed a DAEMON restart that slid the retained digest window past the armed cursor. NOT a gap and NOT this req's property failing: spt-core raised after_predates_window and the consumer took its specced armed-cursor branch (alchemy REQ-TAG-SCANNER missed-rows leg — warn the owner, never silently re-baseline an armed cursor; alchemy src/tags.rs, two unit tests) — the FIRST FIELD FIRING of a path previously exercised only by unit tests. The 'no re-baselined cursor' gate property is hereby scoped: byte-identity holds across SHELL death within one daemon generation (the prior LEG 5 measurement, one daemon instance, uncontradicted — different scenario, generation never moved); across a daemon restart that slides the window the contract is DETECT-AND-ANNOUNCE, not preserve — corroborated independently by a second record's cursor decomposing under the same gen:seq scheme (4294967486 = 1:190). Practical exposure in this event: nil (every agent stood down through the gap; no tag traffic to miss). SECOND SPECIMEN HELD, not spent (doyle ruling 2026-07-26): liam's record — precondition verified by flynn (pid 35464 a CLEAN corpse, not recycled, so the accepted-at-mint recycled-pid caveat does not apply; same poisoned status=online shape; binding intact; cursor armed at 4294967486) — is deliberately NOT relinked while its owner is absent: the admit gate is already field-proven, a relink measurably re-baselines the cursor across a daemon-restart boundary, and liam's is the only untouched specimen with the longer (~8.5h) gap — spending it would destroy the cursor datum liam's own return produces, to buy a redundant second sample of a gate already proven to admit. It waits for liam. SPECIMEN HARVESTED (liam's own return, 2026-07-26 — operator-authorized endpoint start executed by doyle; pre-state snapshotted by doyle AND independently re-verified on disk by liam before touching anything): (1) relink ADMITTED at the ~8.5h gap — verbatim SHELL_RELINKED:alchemy-0 owner=liam pid=47608 status=offline (online at bind); the poisoned record did not refuse; info.json online is now TRUE. (2) integrity FULL PASS: id unchanged (no new mint slot), perch onlined at bind, repo binding byte-identical (token_provenance gh-cli), and the ROTATION-CARRIES-THE-DURABLE-CHANNEL machinery observed live in the field for the first time — link.token.retired present beside the fresh mint, and a post-relink round trip proven end-to-end (shell cmd spooled -> engine executed -> reply home on the owner's perch), not merely an outbound spool. (3) cursor, the sharpest datum of the harvest: BYTE-IDENTICAL through the relink itself (4294967486 armed at pid-change instant, doyle's file watch) — the relink preserves; the SCANNER moves. First scan re-baselined 1:190 -> 2:0 silently, and liam PROVED no-fire-by-precondition rather than assuming: the daemon restart bumped the generation but EVICTED NOTHING (window floor 4294967464 < cursor 4294967486, every gen-1 row past the cursor still retained), so after_predates_window is correctly false at every link of the chain (filter_after floor test -> emit-only-when-true -> unwrap_or(false)) and the missed-rows announce had no precondition to fire on. Flynn's earlier gen-bump reading is refined by this: a restart alone does not slide the window; flynn's announce fired because HIS window had genuinely slid. Scope note carried: a valid negative — the fix's announce chain verified link-by-link to contract on a specimen where it correctly stayed quiet. RESIDUE from the harvest (liam's find, alchemy lane, no core REQ): permanently-unsealable turns from a dark session are jumped by scanner cursor advance with no possible missed-rows notice (a generation bump is not a slide); ruled scanner-side announce for input-bearing null-seq jumps, pseudo-turns jump silently by design; core's per-turn input/input_seq/partial already suffice to discriminate.
2026-09-06T13:09:02.0997283Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.0997449Z 
2026-09-06T13:09:02.0997574Z ### REQ-SHELL-ADAPTER-OWNED-DETACHED-SERVICE
2026-09-06T13:09:02.1016592Z - Title: RULING OWED (unratified — do NOT satisfy without one): may a shell adapter own a DETACHED process that deliberately OUTLIVES its shell's link-break? Today the answer is 'no, silently, on Windows only'. `shellhost::kill_shell_pid` force-kills with `taskkill /PID <pid> /T /F` — /T is a TREE kill, so `close_shell` takes the shell's descendants with it; the Unix arm is `kill -9 <pid>`, a SINGLE process. So the same adapter's detached child dies on Windows and survives on Linux, undocumented either way. Two ratified positions collide here and neither may be quietly discarded: (a) CONTEXT §Shell model lifecycle states flatly 'the binary never survives a link-break', and the KNOWN-HAZARDS 'stop lies' RCA (two field cases 2026-07-19, one on doyle's own production endpoint) was paid for precisely because a surviving GRANDCHILD was orphaned where 'even a direct-child kill would have missed it' — that hazard is the standing argument FOR /T; (b) a node-singleton service that must not drop every time a shell relinks is a legitimate shape, and flynn's alchemy ships one today (Hub Daemon, CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP, lockfile-guarded), depending on surviving link-breaks to keep a Discord gateway up. Note CREATE_NEW_PROCESS_GROUP does NOT sever ParentProcessId, so it is not an escape from /T — the survival flynn depends on is unverified, not designed-for. MEASURED, not theorized (flynn, this node, 2026-07-25, current spt-core, no branch): the Hub Daemon's ParentProcessId WAS the shell's pid — a direct child, so `/T` reaches it by either reading — and `spt shell teardown alchemy-0` took the daemon to zero processes within 0.1s, still zero across a 15s poll. Nothing restarted it (no bind happens in that window and the adapter's ensure runs only at bind, so 'died and restarted fast' is excluded by construction); recovery needed an operator spawn + re-bind. BLAST RADIUS, the reason this outranks its filing: that daemon also runs alchemy's Watch engine, so tearing down whichever shell happened to parent it silently stops State-change notification delivery NODE-WIDE, for every registered Watch and every owner — cross-Project blocking waits simply never fire, with no error anywhere. It satisfies the LETTER of alchemy's own REQ-HAZARD-WATCH-SILENT-LOSS (the registry entry survives) while its INTENT fails (the delivery engine is dead) — filed separately adapter-side. Option space for the ruling: (1) adapter-owned detached services are UNSUPPORTED — such a service is an AlwaysOnEndpoint (REQ-EP-8 / ADR-0023: resident, daemon-supervised, addressable, independent of agent liveness), NOT a shell's descendant. NOTE this is not a new design: ADR-0023 was ratified 2026-06-21 and EXPLICITLY rejected 'make it a Shell' ('shells are single-owner, driven, control-exclusive; a multi-party bridge inverts every one of those invariants'), and named a Discord bridge as its first consumer — i.e. flynn's exact shape. The gap is DELIVERY (REQ-EP-8 is inactive pending its milestone), not design. (2) allow it behind an explicit manifest declaration that exempts named descendants from the tree kill AND obliges the adapter to reap them — viable only as an EXPLICIT INTERIM with (1) as the stated target, never a permanent answer, because permanence would re-adopt the shape ADR-0023 already rejected. (3) status quo, but then DOCUMENT the platform asymmetry and stop claiming 'never survives' unqualified. CONSUMER LEAN IS (1), argued against his own convenience (flynn 2026-07-25): the service is a shell's descendant only because a shell was the only thing he had that could start something; (2) buys that accident a manifest field plus a reaping obligation he must then honor forever, and leaves 'the next shell that happens to bind' responsible for the service being up — which IS the defect he measured. Available to a consumer TODAY without any core change and without an exemption: start the service out-of-band so no shell parents it (unsupervised, but never tree-killed) — the trade is 'unsupervised but safe' against 'supervised by accident and killed by design'. Whichever wins, the Windows/Unix divergence is a defect in its own right and closes with it — `/T` on one platform and single-pid `kill -9` on the other means adapters cannot reason about descendant lifetime at all. Filed by flynn 2026-07-25 as 'flagging it now rather than after'; code-read by doyle, then field-confirmed by flynn at the cost of one teardown. FIELD CORRECTION (flynn, 2026-07-25, alchemy v0.3.1): the measured kill was NOT /T over-reach — the 'detached' daemon was still the shell's DIRECT CHILD in the process table (CREATE_NO_WINDOW / CREATE_NEW_PROCESS_GROUP / null stdio detach the console and the signal group, never parentage), so any tree walk found it by construction; isolated A/B on this node confirmed a grandchild whose intermediate has exited survives /T. alchemy v0.3.1 ships the fix as one relay hop (the binary re-invokes itself, the intermediate exits, the daemon's ParentProcessId is dead and nothing can walk it) — production-verified (daemon pid 35472, ParentProcessId 11108 dead) — and WITHDREW the Scheduled Task autostart entirely (alchemy ADR-0012 now Rejected; ADR-0013 records the relay hop). alchemy no longer pushes on this seed. What remains core-side is narrowed, not changed in kind: /T's breadth (it reaches detached descendants of any adapter that has NOT done the relay hop) is a hardening item on its own merits, and the Windows/Unix kill asymmetry stands. Job-object caveat filed separately — the relay hop does NOT escape a Job Object: see REQ-SHELL-CLI-SPAWN-JOB-EXPOSURE. PREMISE MOVED UNDERNEATH THIS SEED (BAROMETER, todlando 2026-08-02, REQ-SHELL-KILL-AUTHENTICATED — the ruling itself is UNTOUCHED and still owed): position (a) quotes CONTEXT's flat 'the binary never survives a link-break', and that sentence no longer reads flat. The force-close is now authenticated against the pid+birth pair, so a kill whose target cannot be proven ours is refused and named rather than fired. Option (3)'s demand to 'stop claiming never survives unqualified' is therefore already satisfied in part — but for a DIFFERENT reason than this seed argues (target IDENTITY, not the platform asymmetry), and it changes nothing about the option space here: the Windows /T tree-kill versus Unix single-pid `kill -9` divergence this seed exists to rule on is exactly as it was, and an authenticated kill still takes the whole subtree when it does fire.
2026-09-06T13:09:02.1032969Z - Required stages: 
2026-09-06T13:09:02.1033087Z 
2026-09-06T13:09:02.1033198Z ### REQ-SHELL-CLI-SPAWN-JOB-EXPOSURE
2026-09-06T13:09:02.1048709Z - Title: HARDENING SEED (inactive — no defect claimed): a CLI-side `spt shell spawn` rises inside the launching terminal's Job Object when that job denies breakaway. Launch-path census (2026-07-25): `spt shell spawn` calls `shellhost::launch_shell` IN the CLI process, so the shell binary is spawned by a terminal-resident process and is subject to whatever job the terminal wrapped the CLI in; daemon-side (re)launches (`shellwake` relaunch-on-wake, linkhost relink) spawn from the daemon, which is job-neutral once its own cold-start ladder (WMI → schtasks → breakaway → in-job; REQ-HAZARD-VIEWER-CLOSE-DETACH) escaped. The shared spawn primitive (`daemon::detached_no_inherit`) requests CREATE_BREAKAWAY_FROM_JOB best-effort: a job without JOB_OBJECT_LIMIT_BREAKAWAY_OK denies it (ERROR_ACCESS_DENIED) and the DELIBERATE fallback spawns IN-JOB with the loud diagnostic 'DETACH_BREAKAWAY_DENIED: launching Job Object forbids breakaway; spawned IN-JOB (may be reaped if the launching terminal closes)' — chosen so a spawn that once worked never regresses to failure. spt CANNOT grant itself breakaway: BREAKAWAY_OK is a limit only the job's CREATOR (the terminal) sets on the job; a member process has no say. CONSEQUENCE, field-observed (flynn 2026-07-25, verbatim diagnostic in hand, alchemy #22): job membership is inherited by EVERY descendant regardless of parentage — an adapter's detached service that survives the /T tree kill via a relay hop (dead ParentProcessId, alchemy ADR-0013) does NOT thereby escape the job, so if the job sets JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, closing the launching terminal reaps shell + detached service together, relay hop notwithstanding. Whether any given terminal's job sets KILL_ON_JOB_CLOSE is node-dependent and was NOT measured — the exposure is conditional; the diagnostic is the tell. SELF-HEALING PROPERTY, already true today: the exposure is launch-path-scoped, not instance-permanent — the first daemon-side wake/relaunch re-spawns the binary from the job-neutral daemon while the same canonical id + perch survive (REQ-HAZARD-SHELL-STALE-ONLINE recovery), so cycling an instance through a daemon wake is the zero-code mitigation available now. Option space for the ruling: (a) route CLI-side launch_shell through the same job-neutral ladder the daemon cold-start uses; (b) broker the first spawn through the daemon so no shell ever rises terminal-jobbed; (c) status quo + publicly document the diagnostic as contract, so adapters can read DETACH_BREAKAWAY_DENIED as 'this instance is terminal-coupled until its first daemon wake'. Seeded from flynn's observation; census + fallback semantics code-read by doyle at seed time. FIELD FIRING (2026-07-25 17:47, this node — flynn's owlery fact set + doyle's broker-stderr forensics): the conditional exposure is no longer unmeasured — one field event consistent with KILL_ON_JOB_CLOSE firing. At daemon-stop-begin, flynn's shell (last pid 29036, relaunched 17:15 from flynn's context) and the relay-hopped alchemy Hub Daemon (pid 35472, spawned 16:51 off a DIFFERENT, earlier shell — parent long dead) died in ONE two-second window (17:47:56–58), both abruptly with bookkeeping unrun (info.json still status=online on disk), while the broker itself kept draining to final exit 2m10s LATER (stderr abrupt-end 17:50:05.8) — so the closing job's handle owner was NOT the broker; it was some stop-begin casualty (leading read: the restart-initiating context, see REQ-DAEMON-RESTART-SELF-INTERRUPTION). Disjoint parentage + disjoint spawning contexts + one instant means the job spanned processes from DIFFERENT agent contexts — an INFRA-level job, not a per-terminal one — upgrading the worst case from 'this instance is terminal-coupled' to 'one infra job-close can sweep every shell-descendant on the node at once'. The handle owner is unpinnable post-mortem (4689 process-termination auditing off on this node); recorded open. ⚠ CORRECTION 2026-07-26 (doyle, source-verified answering liam — supersedes this title's launch-path census and its SELF-HEALING claim): local `spt shell relink` AND local wake ALSO spawn IN-PROCESS from the CLI — `spt_daemon::relink_shell` is a LIBRARY call in the CLI process (cli.rs ~12559) → `launch_shell` → `detached_no_inherit` runs its breakaway attempt in the CALLER, so the child inherits the terminal's job on denial; ONLY the cross-node arm (`ref@node`) and daemon-originated relaunches (`shellwake` from the daemon, reconcile) spawn daemon-side. Therefore 'cycling an instance through relink' is NOT the zero-code mitigation for a terminal-invoked local relink — the recorded self-heal holds only for daemon-ORIGINATED wake/relaunch, and a DETACH_BREAKAWAY_DENIED on CLI stdout is local truth about THIS spawn, not a relayed daemon diagnostic. FIX RULED IN (FIELD-TRUTH W1, doyle 2026-07-26, option (b) of the option space scoped to the reachable case): when a daemon is RUNNING, CLI-side `shell spawn`/`relink`/local-wake route the spawn THROUGH the daemon (job-neutral by the cold-start ladder) instead of spawning in-process; daemon not running → today's in-process path UNCHANGED as the fallback, with the diagnostic upgraded to name WHOSE job coupled the child (the launching terminal's, via this CLI process) so a field report can distinguish the paths. The first-spawn-brokered variant (option (b) full) and ladder-in-CLI (option (a)) stay open beyond the reachable-daemon case.
2026-09-06T13:09:02.1062579Z - Required stages: impl, unit
2026-09-06T13:09:02.1062741Z 
2026-09-06T13:09:02.1062831Z ### REQ-DAEMON-BITS-AMBIGUITY
2026-09-06T13:09:02.1081488Z - Title: SEED (inactive, RCA-first — do NOT close on agreement): nothing on a node surfaces WHICH BITS ARE SERVING, and the same silent wrong-state shape bit twice in one day (2026-07-25). Case 1 (adapter-side echo): a post-reboot ensure race left a dev-build alchemy Hub Daemon serving release shells — version skew visible only by manually comparing process image paths. Case 2 (core, field-measured by flynn): TWO spt daemons resident with live brains on one node — the installed main daemon (owning ALL sockets: the 5474 listeners and every established connection, single home_tag pipe family) and an orphaned scratchpad-built daemon (auto-started into the node by ensure_running from a stray dev-binary invocation at 16:18, holding zero sockets, resident for hours) — while `spt --version` on any binary file answers nothing about which process is answering. Measured sharp edges to carry into the RCA: (a) exe path and resolved HOME are independent — the orphan ran scratchpad bits against the DEFAULT home, so 'where the binary lives' predicts nothing about 'whose state it mutates'; (b) the brain.ready breadcrumb is ONE FILE PER HOME, LAST-WRITER-WINS, keyed by generation — with two brains in one home the stamp can be written by the daemon you are NOT gating on, so any readiness/identity gate that trusts it must first establish single-writer; (c) the breadcrumb's exe_hash (SHA-256 of resident bytes captured at process start) is the RIGHT discriminator — image path answers what is on disk, not which bits are answering — but only under (b)'s single-writer precondition; (d) reap order matters: killing the breadcrumb's last writer leaves the file describing a dead brain's bits until the survivor's next ready write, so any bits-gate readback must be re-established AFTER a reap, never carried across one. Open RCA questions before any fix is designed: why did the second daemon's cold-start not refuse against the live singleton (socket-bind loss is survivable-and-resident today — is that the right posture?); what should ensure_running check BEYOND socket liveness (bits identity?); where does 'which bits are serving' surface to an operator (endpoint list? daemon status verb?). Kin: the NEVER-SEALING-OBSERVABILITY candidate (same shape — silent wrong-state only a human staring at the right field catches). Proposed by todlando (his lane), relayed by flynn with the socket-ownership + exe_hash measurements; seeded by doyle. The orphan pair was reaped by path 2026-07-25 (verified by exact ExecutablePath, supervisor before brain); the reap resolved the instance, not the class. RCA POINTS FROM THE PROPOSING LANE (todlando, extended into THIS record 2026-07-26 rather than minted as a second seed): (1) ORDERING — 'do both pids resolve the same spt_home?' is the FIRST question, not a co-equal fact, because every other discriminator is conditioned on its answer: brain.ready is `<spt_home>/brain.ready`, ONE path, single-writer BY DESIGN, so a shared home makes the breadcrumb a contended file and `generation` — the readiness gate's key, which exists precisely to prevent false promotion — becomes satisfiable by the stamp of the daemon you are NOT gating on. Prior art on this node: default-home `home_tag` sockets already cross-talk the live daemon's hubs, so shared-home cross-talk is an established class here, not a hypothetical. (2) GATE ON RESIDENT BYTES, NEVER ON IMAGE PATH — AND TREAT ABSENCE AS UNPROVEN. Path answers 'what is on disk where I asked'; with two daemons on different bits the only question that matters is 'which bits answered me'. Path is the exact field that has already lied in the field: KH 6.11 — the broker resolves `current_exe()` PER SPAWN (`crates/spt-daemon/src/brainproc.rs`), which on Linux is inode-tracking, so an `update apply` rename made the respawn land on the OLD bytes while readiness passed and the trial recorded `applied:N` (kitsubito v0.4.1); `exe_hash` (lowercase-hex SHA-256 of resident bytes, captured ONCE at process start — `current_exe_hash`, `crates/spt-daemon/src/brainproc.rs:402`) exists BECAUSE the path-derived belief was provably wrong, and the enlyzeam 0.3.0-under-0.3.2-on-disk case is the same record/reality divergence one layer up. Constraint any observability gate must inherit and must NOT weaken: `exe_hash` is ADDITIVE/BEST-EFFORT — omitted when the self-read fails, `None` on any pre-D7 stamp — and today's `bytes_gate` deliberately degrades an absent hash to readiness-only promotion with a loud `PROMOTE_BYTES_UNVERIFIED` (N-1 compat, `brainproc.rs:1042`). An operator-facing 'which bits are serving' answer must degrade the OTHER WAY: absent hash reads UNPROVEN — never PASS, and never a silent fall back to image path. A compat degrade that is correct for ACCEPTING an update is wrong for ASSERTING an identity. (3) SERVICE OWNERSHIP IS DECIDED BY SOCKET/HUB BINDING, NOT BY START TIME. Case 2 measured the instance (main holds all 5474 listeners + every established connection; the scratchpad daemon holds zero sockets and was the LATER start) but the INFERENCE RULE is what must survive the instance: a later start is not thereby the loser and an earlier start is not thereby the server — 'who is serving' is answered by who owns the binding, so any gate, reap, or diagnostic that ranks candidates by pid or start time is guessing at the one fact it is supposed to establish. (4) REAP-ORDER SYMMETRY — (d)'s re-establish-the-gate-AFTER-the-reap rule holds identically on the REVERT path; a bits-gate readback may never be carried across a reap or a rollback in either direction. (5) SINGLE RECORD — this seed is the one home for the class (premature-closure guard: convergent reads are not a root cause, and a green re-read after a reap assigns owner without closing); the socket-bind-loss posture question stays OPEN inside it, and the orphan-pair instance stays closed.
2026-09-06T13:09:02.1096917Z - Required stages: 
2026-09-06T13:09:02.1097107Z 
2026-09-06T13:09:02.1097222Z ### REQ-DAEMON-RESTART-SELF-INTERRUPTION
2026-09-06T13:09:02.1104080Z - Title: SEED (inactive — operator-workflow trap, field-paid 2026-07-25): an spt-hosted context CANNOT restart the daemon, because the restart's STOP phase tears down the very context running the restart command — the START half never executes and the daemon stays down until an operator starts it by hand. Field case (this node, 17:47–17:50): an agent-kicked restart's stop-begin swept the initiating context and everything sharing its Job Object (flynn's shell + the relay-hopped alchemy Hub, cross-context job members — see REQ-SHELL-CLI-SPAWN-JOB-EXPOSURE's field-firing record); the broker drained ~2m10s to final exit (stderr ends abruptly at 17:50:05.8 amid routine traffic, no shutdown markers); the START was already dead; the operator manually started whatever binary was installed (v0.42.0), with no channel conveying which bits the restarting agent had intended — binary choice silently transferred to whoever brings the daemon up (kin: REQ-DAEMON-BITS-AMBIGUITY, the same which-bits-serve blindness). CONSEQUENCES: (a) every RESTART-REQUIRED apply (broker update, seed-skew restart per hazard 7.9) is operator-only today, and nothing documents that; (b) stop-begin kills bound shells with `close_shell` unrun, so even the GRACEFUL stop path manufactures stale-online shell records on every restart — healed at the read gates by REQ-HAZARD-SHELL-STALE-ONLINE's derivation, but manufactured nonetheless, and on pre-derivation binaries the records lie until the next bind; (c) a restart initiated from inside the system it restarts is a self-interruption with no error surface — the initiator dies believing the restart is in flight. Option space at activation: (1) daemon-owned restart verb — the OUTGOING broker spawns its successor job-neutrally (the 7.10 WMI→schtasks→breakaway ladder already exists) before exiting, so the caller only requests and its death is irrelevant; (2) a detached job-neutral restart helper (same detached_no_inherit + ladder) that survives the caller; (3) status quo + the public contract documents restart as operator-only. Whichever wins must also answer where the INTENDED-BITS declaration lives so a manual or automated starter picks the right binary (the (c)/BITS-AMBIGUITY joint).
2026-09-06T13:09:02.1104422Z - Required stages: 
2026-09-06T13:09:02.1104451Z 
2026-09-06T13:09:02.1104567Z ### REQ-WHOAMI-EXPLICIT-SID-REFUSAL
2026-09-06T13:09:02.1116309Z - Title: RULED DESIGN, delivery unowned (doyle 2026-07-26): when a caller hands identity resolution an EXPLICIT non-empty $OWL_SESSION_ID that resolves to NO perch, core must REFUSE identity (unresolved, exit 1, loud distinct diagnostic) rather than fall through to an ambient/inherited one — today `detect_self_id` (roster.rs, legs a→b→b2→c) treats sid-UNMATCHED identically to sid-ABSENT, so the fallback chain re-adopts precisely the identity a sharper claim just failed to prove. MEASURED (perri, this node, 2026-07-26, three read-only whoami calls from a genuine descendant of the perri host process): (1) all SPT_*/OWL_* scrubbed → id null, exit 1 — ancestry resolved nothing (caveat honored from the probe: the perch's recorded pid was not in the caller's chain, so this run refutes lineage-as-the-mechanism for probe v1 without disproving a lineage path in general); (2) inherited SPT_ENDPOINT_ID=perri + explicit OWL_SESSION_ID matching no perch → perri, exit 0 — the mismatch datum was IN HAND (core had already scanned and failed to match the explicit sid) and the ambient id won anyway; (3) real OWL_SESSION_ID with endpoint id scrubbed → correct self — the healthy path any fix must leave untouched. SCOPE OF THE REFUSAL, ruled: only sid-PRESENT-AND-UNMATCHED poisons the fallback, and it poisons ALL weaker legs (b SPT_AGENT_ID, b2 SPT_ENDPOINT_ID, c pid-ancestry) — an explicit failed claim outranks every ambient claim below it; sid-ABSENT/empty keeps today's full chain unchanged, because the leg-b2 field root (live-repro'd 2026-07-10: the adapter surfaces OWL_SESSION_ID to the session shell as an UNEXPORTED var, so the child process legitimately carries endpoint id without sid) is exactly the flow the guard must not break — that flow is sid-absent, never sid-mismatched. WHY CORE AND NOT ONLY THE ADAPTER: the measured entry path is closed adapter-side (perri's REQ-HAZARD-INHERITED-IDENTITY-ADOPTION @ their a7558aa + claude-spt KNOWN-HAZARDS 7.4, shipped: whoami child calls scrub SPT_ENDPOINT_ID/SPT_AGENT_ID; rig discipline now detached AND env-scrubbed — the scrub is the operative half), but the inconsistency being fixed is CORE'S: the adapter fastpath's verified_env_id REFUSES an inherited SPT_ENDPOINT_ID on carrier-proof mismatch (carrier sid != payload sid) and core's fallback then RE-GRANTS what that layer just refused — one layer's refusal must not be another layer's grant, and every other harness/adapter gets the defense only if core holds it. SEVERITY UPGRADE recorded at mint: unlike the KH 7.1–7.3 shapes (lost reads), this adoption was WRITE-CAPABLE in the field — the adopting descendant re-pointed the ANCESTOR's session pin, so the ancestor went dark while the descendant looked healthy (perri's own pin, probe v1). KIN, same review same milestone: leg (b) SPT_AGENT_ID returns UNCONDITIONALLY today — not even perch-checked, weaker than leg b2's bound-perch gate; align it when this lands. MEASURED on the live node, not just code-read (perri, same probe run, reported 2026-07-26): SPT_AGENT_ID=nobody-xyz with a bogus OWL_SESSION_ID returned {'id':'nobody-xyz','ready':false,'alive':true,'unbound':false}, exit 0 — a phantom identity for an endpoint that does not exist, beating both the sid leg and ancestry; the returned shape has no state key and ready:false but a populated id, and the adapter parser takes .id first, so a whoami-trusting adapter writes state under the phantom — the same write-capable class as the inherited-adoption case, sourced from a made-up name instead of a real ancestor. perri's adapter scrub covers SPT_AGENT_ID as well as SPT_ENDPOINT_ID for exactly this reason. POSTURE UNCHANGED: whoami legs remain from-label/routing only, never authentication (KH 7.3/7.5, F-024 stays parked) — refusal tightens label discipline, it promotes nothing to auth. Gate at activation: unit — sid-unmatched + ambient endpoint id present → refusal with the distinct diagnostic (probe shape 2 goes loud); sid-absent + ambient endpoint id on a bound perch → still resolves (leg-b2 field root preserved); sid-matched → unchanged (probe shape 3).
2026-09-06T13:09:02.1116656Z - Required stages: 
2026-09-06T13:09:02.1116685Z 
2026-09-06T13:09:02.1116793Z ### REQ-SHELL-LIST-DERIVED-PROVENANCE
2026-09-06T13:09:02.1120293Z - Title: SEED (inactive — observability, perri-backed 2026-07-26): a status a reader can act on should be distinguishable from a status a record actually holds. Since REQ-HAZARD-SHELL-STALE-ONLINE, `shell list` renders DERIVED online-ness (recorded status AND pid-liveness), deliberately healing poisoned records at the read gate — but nothing marks WHICH rows were healed, so an agent doing forensics reads the healed view and infers a clean record (field, twice on 2026-07-26: liam inferred close_shell had run when the raw info.json still said online over a corpse pid and proposed spending flynn's live instance to manufacture a specimen they already owned; perri independently named the same misread class from their own work). Shape at activation: an additive marker on the --json row when derived != recorded (e.g. recorded_status alongside status, or derived=true) — additive-evolution posture, text view unchanged or minimally annotated; NEVER a behavior change to the derivation itself. Kin: REQ-DAEMON-BITS-AMBIGUITY (silent wrong-state a human catches only by staring at the right field) and the NEVER-SEALING-OBSERVABILITY candidate — same class, view-vs-truth.
2026-09-06T13:09:02.1120513Z - Required stages: 
2026-09-06T13:09:02.1120537Z 
2026-09-06T13:09:02.1120784Z ### REQ-RESIDENT-SERVICE
2026-09-06T13:09:02.1188861Z - Title: ResidentService substrate (ADR-0049, design ratified 2026-07-26): a daemon-supervised binary an adapter declares via a `[service]` manifest section — core-owned from birth, NO perch/identity/address. SPAWN: the daemon launches it job-neutrally (detached_no_inherit + the cold-start ladder posture), so it is never a shell's child (`/T` tree-kill cannot reach it; the shell-descendant hazard class of REQ-SHELL-ADAPTER-OWNED-DETACHED-SERVICE never arises) and never inside a launching terminal's Job Object (the REQ-SHELL-CLI-SPAWN-JOB-EXPOSURE service half closes by construction). START TRIGGER declared in the manifest: start = 'boot' or start = 'bind'; supervised identically once running, with the wake-watcher scaffolding (backoff, give-up latch, one-per-instance lock, orphan-kill, brain-side reconcile) minus the offline-only flip. 'boot' is DESIRED-STATE-RUNNING, not an event: the supervisor reconciles a boot service toward running at EVERY opportunity — daemon boot, ADAPTER REGISTRATION while the daemon is live (operator addition 2026-07-26: installing or registering an adapter whose manifest declares a boot service starts it THEN — spt itself is never restarted to bring a new adapter's service up), update-hold release, and first shell bind as the defensive ensure. 'bind' starts only at the adapter's first shell bind. CARDINALITY: one supervised instance per registered adapter-option `<adapter>[:profile]` (consumer-confirmed as COHERENT TARGET SHAPE — flynn's precision, 2026-07-26: not exercisable by the first consumer until per-option config dirs exist; alchemy today has one config dir and daemon.toml carries exactly one guild_id); the adapter may keep its own kernel file lock as a private double-start guard — core neither reads nor depends on it. PER-OPTION IDENTITY IS THREADED (flynn's gap, accepted): the supervisor passes the adapter-option name and the per-option runtime dir into the service's spawn environment, so an adapter can scope its private guard AND its config per option — the mechanism that makes two-options-two-services deliverable rather than merely permitted. Without it, two options resolving one adapter config dir produce the silent flap flynn derived: instance two exits immediately on the kernel lock, core sees only start-then-die, and crash-relaunch backoff is CORRECT behavior against that observation — two correct components disagreeing about the unit. UPDATE IS A FIRST-CLASS SUPERVISOR OPERATION WITH AN EXPLICIT HOLD: quiesce -> hold (stopped and NEVER relaunched while held) -> bits swap -> start new bits -> release; adapter update-apply performs this ordered operation; crash-relaunch with backoff applies ONLY when not held. The forcing case is structural, not advisory (flynn, argued against their own convenience): an eager relaunch during a swap re-pins the OLD exe mid-deploy (Windows exe lock), converting a diagnosable os-error-5 into an unwinnable race — if delivery must sequence, the hold ships FIRST and a dead service stays dead until told otherwise (the STALE-ONLINE no-spontaneous-relaunch ruling, same reason, one layer down). FAST-EXIT IS A CONFIGURATION FAULT, NOT A CRASH: consecutive immediate exits (exit within a startup threshold, N in a row) trip the give-up latch EARLY with a distinct loud STARTUP_FAULT diagnostic carrying the captured early stderr — a double-start lock conflict then reads as the configuration fault it is, never as a silently flapping service ground through backoff. QUIESCE IS COOPERATIVE EXIT + DEADLINE: the supervisor places a stop-request marker (a file in the service's runtime dir — polling services observe it on their next cycle; no inbox exists or is added); the service exits WHEN SAFE and the kernel-observed exit IS the ack — 'not ready' is expressed by not-yet-exiting, so no busy record exists to go stale in either direction; a manifest-declared grace deadline (default ~30s) bounds the wait, then force-kill. Delay possible, veto never. An OPTIONAL advisory status line may surface in service status display — never consulted for decisions. LIVENESS IS DERIVED, NEVER RECORDED: the supervisor is the parent and holds the child handle (exit is kernel-observed); no supervisor-maintained running-record exists (the v0.43.0 stale-online lesson applied one layer down — flynn's condition, structural here). Any status/version identity surface keeps the locked-file split lesson: never require reading a file the service holds an exclusive OS lock on (Windows). CLI INVOCATION CAPABILITY (consumer-blocking, non-negotiable per flynn): the supervisor threads the environment so the service can invoke the spt CLI (`spt send` et al., identityless cli@node from-label, durable spooling per ADR-0002) — if a supervised Hub cannot shell out to spt send, node-wide Watch delivery dies silently. ADDRESSING: none — a service needing a two-way agent-facing surface has one at its adapter's endpoint/shell layer (the alchemy layering argument that re-scoped ADR-0023's faceless-service rejection); AlwaysOnEndpoint (REQ-EP-8) = this substrate + the addressable front. Gate at activation (all legs against a MOCK service adapter — the gate never depends on the first consumer being the boot specimen): int — a manifest [service start='boot'] binary rises with the daemon job-neutrally, a registration of that manifest against an ALREADY-LIVE daemon starts the service without any restart, survives a shell teardown of the same adapter (tree-kill does not reach it), a held update swaps bits with zero relaunch races (hold observed under a concurrent crash), quiesce marker -> cooperative exit within grace, deadline -> force-kill on a wedged mock, a mock that exits instantly N consecutive times surfaces STARTUP_FAULT (not a backoff flap), and the service successfully invokes spt send from its supervised environment. ACTIVATED FOR W1 2026-07-26 WITH THE FOLLOWING BUILD RULINGS FOLDED IN (doyle; constraints live in the artifact, not the dispatch thread). VERB SURFACE: the operator-facing verbs are `spt adapter service list` (all registered options + derived state) and `spt adapter service status <adapter[:profile]>`, nested under the ADAPTER group — NOT a bare `spt service`. Reason: 'service' is ALREADY public surface carrying an unrelated meaning — the platform daemon-service abstraction (REQ-DAEMON-6/-8, `crates/spt-daemon/src/service.rs`: the systemd user unit vs the Windows at-logon task) surfaces in `spt daemon` help as 'registered OS service' / 'managed service' / 'the managing service label'. Ownership-scoping separates the two meanings permanently: the OS-service is the DAEMON's and lives under `spt daemon`; the resident service is the ADAPTER's and lives under `spt adapter`. ADR-0049's 'service status display' means `spt adapter service status`; the W1 PR carries a one-line ADR errata note. GIVE-UP LATCH SCOPE: the latch suppresses relaunch grinding until something plausibly changed — it is NOT a durable verdict. Cleared by exactly three events: (1) DAEMON BOOT — desired-state-running enumerates boot as a reconcile opportunity with NO latch exception, and the re-trip is bounded (N fast exits) and LOUD (STARTUP_FAULT re-fires each boot); an in-memory per-daemon-lifetime latch is an acceptable implementation, and if the durable-marker scaffolding is reused then boot clears the marker; (2) ADAPTER RE-REGISTRATION — declared intent that manifest/config changed, reconciling immediately; (3) UPDATE-HOLD RELEASE — new bits invalidate the fault evidence. NOT cleared by the first-shell-bind ensure: a bind changes nothing about the service's config, so the bind-time reconcile SKIPS latched services — otherwise ordinary shell use converts the latch into the very flap it exists to stop. The REJECTED alternative is recorded deliberately: a durable latch with explicit-clear-only leaves an operator's already-fixed config fault sitting behind a service that stays dead and quiet forever — loud-bounded beats quiet-permanent (the heal-assigns-owner-never-closes shape one layer down). SANCTIONED BUT NOT W1-REQUIRED: `spt adapter service restart <adapter[:profile]>` as the explicit manual clear+reconcile — take it into W1 only if cheap once the verb group exists; the three automatic clears ARE the requirement. REGISTRATION-TIME START IS A WIRE OP: `registry::register` runs in the CLI PROCESS (`crates/spt/src/cli.rs`, the adapter-add and adapter-update call sites), so it cannot itself start anything in the daemon — registration-starts-the-service structurally requires ONE new adapter-scoped daemon IPC op, semantically `AdapterServiceReconcile { adapter }` (exact spelling matched to house op style where it lands), which the CLI calls after a SUCCESSFUL register on BOTH paths when the daemon is reachable. The daemon handler runs THE SAME reconcile code path as boot / hold-release / bind — ONE reconcile function taking an opportunity discriminant, never a second start authority. The response is a per-option outcome list (started / already-running / held / latched / bind-deferred / startup-fault) so the CLI prints honest per-option text. Daemon NOT reachable: registration STILL SUCCEEDS — never a refusal — and the CLI prints a REQUIRED notice that the service is declared, the daemon is not running, and it will come up at the next daemon boot. That notice is CONTRACT, not courtesy. RUNTIME-DIR ENCODING: cardinality is per adapter-option, so the option name becomes a PATH component and `:` is illegal in a Windows path. Core NEVER uses the raw option string as a path component — every construction site goes through ONE shared encoder, and that encoding MUST BE INJECTIVE (a lossless escape, never a strip/replace that can collide). Two distinct options mapping to one runtime dir means two services sharing a quiesce-marker namespace: the silent flap one layer down. The unit gate MUST include a collision-adversarial pair (e.g. if `:` maps to `_`, then `a:b` and `a_b` must remain distinct). MODULE PLACEMENT: the supervisor lands as `servicehost.rs` (the established `*host` convention — shellhost, harnesshost, linkhost, applyhost); `service.rs` is untouched and BOTH module headers cross-reference the other meaning of 'service'. INSTALL-DIR RESOLUTION RIDES W1 (operator-requested via flynn, ruled in after code verification): the `[service]` spawn resolves its binary through the EXISTING REQ-INSTALL-11 helper — the same primitive, NO parallel resolution path — and the same wiring lands at the two shell-family fill sites, which today resolve NEITHER the install-dir program token NOR `{adapter_dir}`. Without it a `--release`-installed shell adapter registers but cannot spawn (bare token → os error 2; `{adapter_dir}/x` → 'no value for substitution key'), released shell adapters need a hand-maintained per-node manifest, `spt adapter update` on them is a permanent no-op, and THIS req's hold/swap/start ceremony would be exercisable by MOCK ONLY — never by the named first consumer. Site census discharged BEFORE build (authoritative grep, cfg(test) excluded, accepted by doyle): the production template-fill sites are `shellhost.rs::fill_spawn_command` and `shellwake.rs::fill_wake_command` (both targets — threading not shape, since the wake caller already holds `AdapterRecord.source_dir`, which IS the install dir), plus `harnesshost.rs` session `role.command`, which is OUT OF SCOPE and already resolves correctly through `resolve_program_in_dir`. Scope guard: shell spawn/wake + `[service]` ONLY — this does NOT expand into the `[session.self]`/`[history]` follow-on sites. FAULT COUNTERS ARE TWO, NOT ONE (ruled 2026-07-26 after the builder surfaced the reading): (1) the FAST-EXIT counter increments ONLY on exits whose uptime is UNDER the startup threshold, and RESETS the moment any run EXCEEDS that threshold. Without the reset the latch mislabels slow-crash decay as a configuration fault — the latch lying about cause, which is worse than no latch. STARTUP_FAULT is reserved for THIS path alone. (2) The ORDINARY consecutive-crash give-up (the shell wake-watcher scaffolding's `give_up_after` = 6, deliberately UPTIME-BLIND) sits BEHIND it UNCHANGED in W1 and keeps its existing diagnostic label — no silent behavior fork from the shell watcher. (3) BOTH counters reset on the latch-clear events above (daemon boot, adapter re-registration, update-hold release): a clear that left either counter primed would relatch on the first post-clear crash and thereby defeat the clear. (4) NOTED-OPEN, deliberately NOT W1 and NOT a promise: the ordinary counter's uptime-blindness means a service that crashes once a day gives up after six days and then stays down until a clear event. Whether that decay behavior is right for SERVICES (as opposed to the shell watchers it was designed for) is a future ruling; it is recorded here as open so the next builder inherits the question rather than rediscovering it in the field. ORPHAN ADOPTION IS IMAGE-VERIFIED, AND ITS PLATFORM GAP IS RECORDED-OPEN (ruled 2026-07-26 after the builder surfaced the trade): a fresh daemon kills a dead daemon's parked orphan ONLY by path-verified identity — a live pid whose image path cannot be READ classifies `Unverifiable` and BLOCKS adoption, i.e. the start refuses loudly rather than proceeding. Loud-blocked over quietly-double-started is this design's whole posture (a bare-pid kill is the recycled-pid class, and a double-start is the silent flap the cardinality rule exists to prevent). RECORDED-OPEN consequence, NOT debt owed by W1: the image oracle is `/proc` on unix, so a unix WITHOUT `/proc` (macOS/BSD) would block on every live orphan until that pid dies. This is theoretical for every platform we ship — win, linux-gnu, musl — and no macOS/BSD asset exists; a future builder adding one inherits the question here rather than rediscovering it in the field. SUPERVISOR PLACEMENT IS BROKER-SIDE (ruled 2026-07-26): the supervised set, its boot sweep and the reconcile control socket live in the BROKER process, beside the digest/drive/tunnel hubs — NOT in the restartable brain child that hosts shellwake. Two reasons, both structural. (a) A supervisor owns LIVE CHILD HANDLES and, from the update ceremony on, an in-memory HOLD: that is a daemon-lifetime continuity resource, which is the ADR-0018 Q2/Q5 broker-side test; the Q5 exception that put shellwake in the brain reads 'a pure disk-reconciler' and this is not one. (b) A brain restart is the ROUTINE UPDATE PATH (StartReason::Update exists precisely for it), so brain-hosting would bounce every resident service through the orphan-adoption path with no quiesce, no grace and no hold — the ungoverned bounce this req's ordered update operation exists to replace — and would lose the hold mid-swap. Broker-hosting is also what makes the wire op possible at all: all control sockets are broker-served because a CLI cannot reach brain memory (stated in drivehub.rs's module header and obeyed by every hub). NO PERIODIC SWEEP — CHOSEN, NOT OMITTED (ruled 2026-07-26): the supervisor host runs the boot sweep and then parks; there is deliberately no timer re-sweeping on a cadence. The four ruled opportunities are all EVENTS, each with a caller that enters the one reconcile function directly, so a timer would be a FIFTH start authority nobody ruled in — and its only distinctive work would be silently healing a failed registration nudge, converting a diagnosable defect into invisible behavior. The REQUIRED daemon-unreachable notice is the honest answer to that case; machinery that papers over its own failure class is refused (the same instrument-soundness razor as the rest of this design). A SUPERVISOR WHOSE DECLARATION DISAPPEARS RECONCILES TOWARD STOPPED (ruled into W1 2026-07-26): every sweep runs a STOP side before its start side — an option whose adapter is soft-deregistered, hard-removed, or whose manifest no longer declares a [service] is torn down through the handle that names its child. This is the symmetric half of desired-state-running, not an addition to it: without it a deregistered adapter's binary outlives its own registration until the daemon dies, which is exactly the ungoverned-lifetime shape this req abolishes. TREE TEARDOWN ON EVERY SUPERVISOR-INITIATED KILL (ruled 2026-07-26): the force-kill deadline is where the unconditional-kill promise is WRITTEN, but it is not the boundary of the problem — a supervised service's descendants are torn down on every kill the supervisor initiates, including daemon-shutdown stop_all and the stop-side sweep teardown. Descendants outliving THOSE paths are strictly worse off than ones outliving a force-kill, because the next daemon's orphan sweep is structurally blind to them: it knows one parked pid and holds no handle to anything below it. Windows reaches the tree through a SUPERVISOR-OWNED Job Object assigned at birth (CREATE_SUSPENDED -> AssignProcessToJobObject -> resume, so no descendant is ever spawned outside the job; KILL_ON_JOB_CLOSE deliberately OFF, so a dying supervisor is never an unannounced service outage); unix through the process group setsid already establishes. Job-NEUTRALITY is not contradicted: that invariant governs OTHER people's jobs reaching our processes, which CREATE_BREAKAWAY_FROM_JOB still handles at birth. A job the OS refuses is a LOUD DEGRADE, never a refusal to start: the process still dies on demand and only its descendants become unreachable, which is exactly where this path stood before the job existed — refusing the spawn would convert a bounded descendant leak into a total outage over a failure in an OS facility rather than in anything the adapter declared. THE TREE PROMISE STATES ITS EDGE (ruled 2026-08-03, IR-16): a teardown whose TREE reach FAILS — as distinct from a job never acquired — does not silently wear the unconditional promise. It announces `SERVICE_TREE_KILL_INCOMPLETE`, naming the LOSS (the tree claim was not honored; descendants may still be running and nothing holds a handle to them) rather than the syscall, alongside the pid, the failed reach and the OS error; the process kill proceeds regardless, so this is a NAMED EXCEPTION to tree-teardown-on-every-kill and never a refusal to kill. It is the kill-time twin of `SERVICE_JOB_UNAVAILABLE` — ONE loss, one vocabulary, both ends of the lifetime, each code naming the other so grepping either finds both. THE SPAWN-SIDE DEGRADE RULING ABOVE MUST NOT BE CITED FOR THIS ARM: it rests on refusal costing a total outage, which is true at spawn and false at kill, where the process kill proceeds either way and refusing the tree claim costs the caller nothing — the two share a noun and not an argument. THE QUIET ARMS ARE A WHITELIST, so an unforeseen failure announces rather than being swallowed: on unix `ESRCH` ALONE is quiet (MEASURED 2026-08-03, Linux 6.8.0-124: `kill(-pgid, SIGKILL)` returns 0 for a live group AND for a group whose only member is an unreaped zombie, and `ESRCH` only once the group is gone and reaped — and since this path never reaps first, `ESRCH` means something else already did, which is the path where the supervisor took its exited branch and never called the teardown at all); on Windows the DIRECT-PROCESS return is not checked at ALL (MEASURED 2026-08-03, Win 11 Pro 10.0.26200: `TerminateProcess` against a handle to an already-exited process returns 0 with `GetLastError` 5 `ERROR_ACCESS_DENIED`, so a check there would fire on the ordinary path), because that arm already carries the strictly stronger downstream liveness confirm behind `SERVICE_KILL_UNCONFIRMED` and a second weaker rule beside a working one is refused. A FALLBACK PROCESS-TABLE TREE-WALK WAS CONSIDERED AND REFUSED as the degraded arm — the ceiling is chosen, not missed: live `(pid, ppid)` descent breaks at the middle hop the teardown itself kills, returning an empty set in exactly the failure case it would exist to detect (a silent loss upgraded to a false clean), and it would kill by bare pid, reintroducing product-side the recycled-pid class. Growing this into an image-authenticated population sweep stays open as its own instrument. DISCRIMINATION: a teardown failure WITH this code is this arm; WITHOUT it, a survivor is a victim of a bare-pid mis-kill — the two must not be blurred by wording. THE STATUS SURFACE IS DAEMON-ANSWERED OR IT SAYS NOTHING (ruled 2026-07-26, the leg-D companion of the tree-teardown ruling): the CLI NEVER derives service state from the pid file. Either the daemon answers `spt adapter service list|status` over the control socket, or the CLI prints that the daemon is not running — verbatim, and with no fallback read. The pid file is a KILL HANDLE for the NEXT daemon, and reading it as liveness in a display surface is the v0.43.0 STALE-ONLINE class one layer down: a record answering a question the record cannot know. Three properties follow and are requirements, not implementation taste. (a) The status op is a PROJECTION over the live supervised set — the supervision threads, the hold flags and the stand-down records — plus the registry; it starts, stops, holds and kills nothing, because a diagnostic that converges the thing it measures is a start authority wearing a diagnostic's clothes. (b) A row reports its EVIDENCE: a latch surfaces with the captured startup output behind it, since a fault reported without its cause is the instrument failing at its one job. (c) An option SUPERVISED WITHOUT A DECLARATION behind it is reported as exactly that rather than hidden — with no periodic sweep, a deregistered adapter's supervisor lives until the next opportunity's stop side reaches it, and that window is precisely when an operator asks what is running. THE ADVISORY STATUS LINE IS DISPLAY-ONLY AND BOUNDED: the service may write one line into its runtime dir (named in docs/MANIFEST.md beside the stop-request marker, so it is implementable); core reads the FIRST line under a byte cap, treats an unreadable file as simply no advisory (the locked-file split rule holds), and consults it for NO decision — core deciding on it would put a least-trusted binary's self-report in the control path, and a service that stopped updating it would silently become whatever it last claimed. WIRE LENIENCY IS PLACED, NOT SPRINKLED: fields a reader BRANCHES on stay typed with a `#[serde(other)]` unknown arm (KH-2.3), while a field only ever ECHOED carries the daemon's label verbatim — re-deriving a lenient copy of the manifest's validated `start` vocabulary for the wire would weaken the one place strictness matters (registration). An op an older daemon predates HANGS UP rather than reading the request and answering nothing: a server that silently ignores an unknown kind leaves the caller blocked on a reply that never comes, so an older daemon would WEDGE a newer CLI instead of failing it.
2026-09-06T13:09:02.1190264Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.1190303Z 
2026-09-06T13:09:02.1190420Z ### REQ-PSYCHE-TURN-STREAM-EVIDENCE
2026-09-06T13:09:02.1193420Z - Title: A failed psyche turn preserves BOTH captured streams as evidence: TurnError::Failed carries the child's stdout alongside stderr, and the failure display appends a bounded single-line stdout TAIL (last ~500 bytes, UTF-8-boundary-safe cut, newlines collapsed, the literal <EMPTY> when the stream said nothing — absence stated, never implied by a missing field). WHY (2026-07-26 spend-limit RCA): a failed turn's stdout is not psyche output, so BOTH core (turn.rs kept {status_code, stderr} only) and the adapter (guarding its outbound channel) independently discarded it — the same blind spot implemented twice — and an account-level outage surfaced as a bare 'claude exited exit code: 1' with the decisive refusal text thrown away at two layers; three agents then chain-hypothesized on an error string the real failing path never emitted. The tail is cause-agnostic instrumentation: it does not care what the failure is, which is why it survives being wrong about it. Adapter twin: claude-spt v0.25.14 dual-stream tail (shipped 2026-07-26).
2026-09-06T13:09:02.1193548Z - Required stages: impl, unit
2026-09-06T13:09:02.1193581Z 
2026-09-06T13:09:02.1193703Z ### REQ-PSYCHE-ACCOUNT-REFUSAL-EXIT
2026-09-06T13:09:02.1200136Z - Title: RESERVED EXIT 96 — account/credential refusal from a psyche_resume turn: the inner tool refused for account-level reasons (spend/usage cap, expired/revoked credential, org quota) — session healthy, code healthy, retry correct-but-pointless until a HUMAN acts. Core discriminates on the EXIT CODE ALONE (text-blind, the exit-95 layering exactly: adapters own text matching because their inner tool's wording is theirs to track; core's contract survives any rewording). SEMANTICS ruled 2026-07-26: (1) OWN PACING, fully separate from the C3(b) strike budget — an account refusal fails FAST (refused before a billed turn), so ten near-instant cycles could exhaust the defect budget in seconds and kill the psyche host as a thrashing component while nothing thrashes; the strike budget is a DEFECT budget and an outage must not be able to spend it (fold-with-higher-threshold REFUSED at ruling: it keeps the bug in a quieter form). Slow capped exponential ~60s doubling to ~15m cap, held INDEFINITELY (no give-up: a cap clears on human action or a calendar boundary — unpredictable but CERTAIN — and a permanently-given-up psyche is invisible), reset on first success, no state to unwind. (2) DISTINCT SURFACE: never the defect-shaped PSYCHE_TURN_FAIL prefix — renders as its own class (adapter-side PSYCHE_TURN_REFUSED; core-side an additive class discriminator beside psyche_host_error, never a fold into it), NAMES the actor and action (a human, the inner tool's account — not the endpoint/adapter/session), carries the inner tool's own words via the dual-stream tail (REQ-PSYCHE-TURN-STREAM-EVIDENCE), and states HOW LONG it has been refused. (3) NEVER reseeds — custody is fine; a reseed would destroy a healthy transcript for nothing. (4) Never counted as a crashloop defect. Adapter contract half: claude-spt docs/design/RESERVED-EXIT-ACCOUNT-REFUSAL.md (appendix of observed CC strings is informative-not-contract, populated ONLY from observed bytes — the invented-string chain of 2026-07-26 is the anti-pattern it refuses). Gate at activation: unit — exit 96 classifies refused (no strike increment, no reseed, own backoff schedule engaged); 95/generic/None unchanged; surface renders the distinct label + duration.
2026-09-06T13:09:02.1200476Z - Required stages: doc
2026-09-06T13:09:02.1200510Z 
2026-09-06T13:09:02.1200628Z ### REQ-MSG-INJECT-LEG-DROP-VISIBLE
2026-09-06T13:09:02.1205710Z - Title: SEED (inactive — observability): a silently-dropped delivery leg must be DISTINGUISHABLE from an honestly-offline endpoint on a status surface. The spt-hosted inject leg (spt-daemon inject.rs `try_spt_hosted_inject` — the ONE shared implementation behind local cmd_send, the WAN ingress, the idle-edge drain, and the parked-idle/pulse re-offer belts) gates on `is_spt_hosted_no_relay` → `deliver::is_online` → `liveness::is_perch_alive`: a perch PINNED TO A DEAD SESSION (the KH 7.25 wedge class — dead owner, record not yet healed by the next auth touch) makes EVERY belt on that leg silently return None → messages spool as if the endpoint were ordinarily offline, while the operator-facing view can keep reading the recorded state. Nothing anywhere surfaces 'the inject leg stopped firing for this endpoint' — the idle-window injections just stop, which from the outside is indistinguishable from 'no messages arrived' (field shape: IDLE-EDGE W1 field-verify, perri's killed-resident rig — their 'so it did not repeat' conclusion was exactly this invisibility, self-corrected only by re-rigging with a live resident). Shape at activation: a status/list surface DERIVES and reports the inject-leg verdict per endpoint (inject-target / not-a-target + WHY: no perch, dead session pin, no relay, not controllable) so a dropped leg is a READABLE state, never an inferred one; additive-evolution posture on any --json surface. Kin: REQ-SHELL-LIST-DERIVED-PROVENANCE (view-vs-truth, same class), KH 7.25 (`REQ-HAZARD-SESSION-PIN-WEDGE`, the pin that drops the leg), KH 2.5 (liveness authority lives in ONE resolver — the verdict here REPORTS through it, never re-derives beside it).
2026-09-06T13:09:02.1205847Z - Required stages: 
2026-09-06T13:09:02.1205876Z 
2026-09-06T13:09:02.1205991Z ### REQ-ENDPOINT-STOP-RESOLVES
2026-09-06T13:09:02.1211221Z - Title: `spt endpoint stop <id>` REFUSES an id that nothing on the node knows, instead of stamping success on a no-op — an unconditional-success verb is a lying instrument (find: liam via flynn's discriminating repro, mechanism corrected by flynn 2026-07-26 superseding the original shell-half-action framing; shells aren't endpoints and the verb correctly never tried to resolve one — it then answered incorrectly). TODAY (cli.rs `stop_endpoint_core`, read at mint): ready-marker removal is `.is_ok()`-best-effort, `teardown_hosted_session` topology-gates on a `controllable` flag a nonexistent perch cannot have and falls through, `unregister_address` is `let _`, `terminal_normalize` silently skips a recordless perch — so EVERY string returns `Stopped{removed:false}` → `STOPPED:<id> (no ready marker; address unregistered)` exit 0, and the 'address unregistered' clause prints whether or not any address existed to unregister. FIX SHAPE: resolve FIRST — an id with ZERO evidence on this node (no ready marker, no perch record, no registered address, no broker session row) is REFUSED with a non-zero exit and a line naming that nothing by that id exists here; ANY evidence → proceed EXACTLY as today (stop is the last rung of the teardown ladder and its wedge-breaking semantics on partially-dead state are load-bearing — the refusal must never make a wedged-but-evidenced endpoint harder to kill). STOPPED is claimed only when the verb acted on something that existed. Success-line honesty rides the same change: clauses name what actually happened ('address unregistered' only when an address was removed). Kin: KH 7.49 (a verb never stamps a state it did not cause — this is that hazard's resolve-half), REQ-ENDPOINT-TEARDOWN-AUTHORITY (the ladder whose semantics must survive unchanged).
2026-09-06T13:09:02.1211545Z - Required stages: impl, unit
2026-09-06T13:09:02.1211578Z 
2026-09-06T13:09:02.1211693Z ### REQ-TEST-RIG-DAEMON-TEARDOWN-PROVEN
2026-09-06T13:09:02.1221048Z - Title: releases#99: a test rig that causes a daemon to exist must stop it, and its teardown must be PROVEN to stop it rather than assumed to. Two findings make this a requirement rather than a fixup. (1) OWNERSHIP IS NOT DECLARED BY THE VERB: `api::run` calls `ensure_daemon()` before dispatching ANY subcommand (REQ-DAEMON-3), so a rig whose verbs are pure local reads still owns a real daemon tree one layer out — `psyche_download_e2e`'s own header asserted "no daemon — it is a pure local read" and leaked 2 `spt.exe` per run (the detached daemon plus its brain, measured against a 0 baseline) which hold `target/debug/spt.exe` open and break the next cargo build in that tree with os error 5. (2) THE TEARDOWN CAN REFUSE SILENTLY, BY TWO INDEPENDENT ROUTES. `cmd_daemon_stop` denies any caller that resolves as a hosted endpoint via `ceremony_agent_ground()`, a two-rung ladder: rung 1 is a non-empty `OWL_SESSION_ID` / `SPT_AGENT_ID` / `SPT_ENDPOINT_ID` IN THE RUNNER'S ENVIRONMENT, rung 2 is pid-ancestry against the perch records, which a fixture perch seeded with `std::process::id()` satisfies for every child of the harness. `--force` overrides neither. The rungs fire on DIFFERENT machines — rung 1 is dead on a CI runner and live whenever an agent runs the suite from its own hosted session — so a rig validated on one of them can leak on the other, and in EVERY arm the test still passes: the leak is invisible to the assertion. Gate: unit — the rig seeds pid 0, scrubs the three env markers on the commands it tears down with, runs every leg BEFORE any assertion (a panic must not skip the teardown), and keeps the authenticated breadcrumb reap as insurance BEHIND the stop rather than in place of it. Evidence discipline for any future rig of this shape: measure the surviving-process count on both arms; a green test proves nothing about the leak. Kin REQ-TEST-TMPDIR-HYGIENE (the same class one resource over), REQ-HAZARD-TEST-PID-TREE-KILL-IDENTITY (why the reap is authenticated), REQ-DAEMON-3 (the autostart that creates the ownership), REQ-BROKER-STOP-ENDPOINT-DENY (the refusal being worked around). AMENDED BAROMETER H2 (doyle ruling 2026-08-02, folding the releases#110 code remedy in here rather than onto #110): teardown completeness extends to EVERY SURVIVING DESCENDANT, not just direct children — a grandchild inherits the cwd through its parent and outlives a kill aimed one level up, which is precisely what the #110 positive control caught (an orphaned `PING.exe` still pinning a directory after its `cmd` parent was killed). The measurement discipline is unchanged and already meets the extended scope: surviving-process count on BOTH arms, by image path so a live fleet is not miscounted — the daemon's brain is itself a grandchild, so the 2 -> 0 evidence already spans the depth. SECOND CONSEQUENCE, now proven rather than theorized: a survivor does not merely hold `target/debug/spt.exe` open (os error 5 on the next build), it CWD-PINS the package root cargo handed it, leaving a `.worktrees` skeleton that cannot be renamed or deleted while files inside it stay writable — see KNOWN-HAZARDS 5.18 / REQ-HAZARD-TREE-CWD-PIN for the mechanism and for the two spawn-flag fixes refused on evidence.
2026-09-06T13:09:02.1221415Z - Required stages: unit
2026-09-06T13:09:02.1221443Z 
2026-09-06T13:09:02.1221548Z ### REQ-TEST-ENV-ONE-LOCK
2026-09-06T13:09:02.1232360Z - Title: releases#111: a process-global environment variable is guarded by exactly ONE lock, and every test helper that mutates it takes THAT lock. THE FILED MECHANISM IS REFUTED — do not implement on it. The filing said three dispatch.rs sites go through `crate::test_home::with_home`, which writes the process-global SPT_HOME, and that a SIBLING with_home test retargets the home mid-test under threaded cargo test. That cannot happen as described: `with_home` holds its mutex across its ENTIRE body, so with_home callers are already perfectly serialized against one another. REPRODUCED AND MEASURED INSTEAD (hertz, HFENDULEAM 2026-08-02): `cargo test -p spt-daemon --lib dispatch::` loses 1-3 of 26 nondeterministically, and the panic text names the real root — `ACCESS_STORE_MINTED <tmp>\home\identity\trust\access.json`, a path with a `home` SUBDIR that `with_home` never produces (it sets SPT_HOME to the tempdir root itself). That shape is `spt_test_support::TestHome`, and the lib binary has exactly one caller of it: `dispatch::tests::the_receiver_decides_origin_and_flood_not_the_sender` (dispatch.rs:2536). TWO INDEPENDENT MUTEXES OVER ONE ENV VAR: `spt_daemon::test_home::ENV_LOCK` and `spt_test_support::ENV_LOCK` each serialized their own users flawlessly and guarded nothing between them. Because `spt_store::perch::spt_home()` is NOT memoized (re-reads env every call), a with_home test would `save()` its fixture into home A and load it back from home B — store absent, empty access baseline minted over it, and `an_unadmitted_asker_is_refused_at_this_nodes_own_seam` getting Some(offline) where the rules should have returned None. The other two losers are the same switch seen through a knock store and a byte-identity snapshot. Every loser passes ALONE (single-threaded: no concurrent holder) and 26/26 under nextest (per-test processes), which is why CI's nextest unit lane is structurally blind to it — the filing is right that this burns only whoever hand-runs the module. FIX (far smaller than the previously-ruled `*_at(home)` seam refactor, which was predicated on the refuted story): `spt_test_support` exposes its lease as `env_lock()`, and `spt_daemon::test_home::env_lock` returns THAT guard instead of declaring a second static — one variable, one lock. Explicitly NOT serial_test. Gate: unit — a with_home body holds the lease while a second thread attempts a TestHome, asserting the thread cannot enter and SPT_HOME cannot move; reds on the two-lock code, greens on one, and proceeds after release so a shared lock is proven to be a lock rather than a deadlock. NOT REENTRANT by construction — a TestHome holds the lease for its whole lifetime, so one must never be constructed inside a with_home body; the module doc says so at both ends. SWEEP, because one instance of a class is not the class: every crate declaring an SPT_HOME test helper was checked against whether the SAME binary also reaches `spt_test_support`, which is the only way the collision can form. A SECOND LIVE INSTANCE was found and fixed in the same pass — `crates/spt` carried `testutil::LOCK` while `cli.rs` (two sites) constructed `TestHome` directly, the identical two-mutex shape, latent and unreported; it now takes the shared lease too. The remaining helpers are genuinely single-per-binary and are left alone rather than pre-emptively churned: `spt-live::test_support` and `spt-store::perch`'s in-module helper (neither crate depends on spt-test-support at all), and `spt-msg`, which depends on spt-test-support but declares no helper of its own. The invariant to enforce at review is one-lock-per-variable-per-BINARY — a crate may keep its own ergonomic wrapper, but never its own mutex over a variable another helper in the same binary also writes.
2026-09-06T13:09:02.1232693Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1232722Z 
2026-09-06T13:09:02.1232818Z ### REQ-HAZARD-TREE-CWD-PIN
2026-09-06T13:09:02.1244196Z - Title: releases#110: a directory that is a live process's CURRENT DIRECTORY cannot be renamed or deleted on Windows while files INSIDE it stay writable, so a test descendant that outlives its run leaves an undeletable `.worktrees` skeleton that reads as neither a file lock nor a rig failure. cargo runs each test binary with cwd = the PACKAGE ROOT, so every spawned child — and every grandchild inheriting through it — takes `<worktree>/crates/<pkg>` as its own cwd. THE FILED MECHANISM IS REFUTED AND THE CORRECTION IS THE POINT: #110 named the holder an orphaned `conhost.exe` holding the crate root; measurement on HFENDULEAM 2026-08-02 shows `CREATE_NO_WINDOW` does mint exactly one conhost per console child (counted by ParentProcessId) but that conhost's cwd is OS-assigned, not inherited — a census of every conhost on the box returned 43 readable, all `C:\WINDOWS\` or `C:\WINDOWS\system32\`, ZERO under any worktree (4 unopenable, stated). Positive control on a scratch dir: write-inside OK, delete BLOCKED, kill the CONHOST → still BLOCKED, kill the actual holder → deletes first try; the holder was an orphaned GRANDCHILD (`PING.exe`, cwd inherited through `cmd`) that survived its parent's kill. A CREATE_NO_WINDOW conhost is a byproduct, never the pin — killing it is not the cure and "no conhosts" is not the all-clear. AMENDED 2026-08-02, on field evidence that landed BEFORE this shipped in a golden run: a `--headless` ConPTY conhost is a DIFFERENT population and the word 'never' does not survive it. It inherits the cwd of whoever created the pseudoconsole and can be the SOLE pin — BAROMETER W4 specimen pid 46556, `conhost.exe --headless --width 80 --height 24`, parent dead, PEB cwd inside that worktree's crates/spt, killed and the tree deleted (todlando). Reproduced deliberately through CreatePseudoConsole from a chosen cwd, with the pin's own control (write-inside OK, delete BLOCKED while it lives, DELETED after ClosePseudoConsole) and the pty-teardown alternative excluded by a kill-ORDER rig: client killed first and the directory is STILL pinned, conhost killed and it deletes. Read a conhost's COMMAND LINE before deciding — a bare `conhost.exe 0x...` is never the pin, a `--headless` one can be, and its child count says nothing either way because a ConPTY client is a child of the creator, not of the conhost. Two tempting fixes are refused ON EVIDENCE: flipping the rigs' `CommandNoWindowExt::no_window()` to `DETACHED_PROCESS` removes the conhost (1 → 0) but not the pin, and `job_escape_e2e::conhost_children_of` shells to powershell through that helper with an `unwrap_or(0)` fallback, so under a flag where powershell does not execute at all (KH 5.17 amendment) the `REQ-HAZARD-WMI-DAEMON-WINDOW` guard would assert "no conhost" VACUOUSLY GREEN — a gate whose failure signature is identical to the fault it guards; and the helper cannot hand children a neutral cwd because `bind_cwd_project_e2e` and `resume_template_e2e` assert on the INHERITED cwd, which is the behavior under test. Doc stage carries the triage step that the diagnosis tool cannot: the finder answers WHO pins a directory, never whether it is SAFE TO KILL, and the blocked-delete error is identical for an orphan and for a working agent's own shell (confirmed live — one worktree's holder was an agent's pwsh tool shell with its Bash tool holding a SEPARATE cwd in the same tree; a pattern-matched kill would have cost that agent their session). Kin REQ-TEST-TMPDIR-HYGIENE (which lists 'children pinning the dir cwd so removal fails' as an unproven candidate root — this proves that class), REQ-TEST-RIG-DAEMON-TEARDOWN-PROVEN (teardown completeness is the prevention side, extended to grandchildren), REQ-HAZARD-WMI-DAEMON-WINDOW (the guard the refused fix would have hollowed out).
2026-09-06T13:09:02.1244524Z - Required stages: doc
2026-09-06T13:09:02.1244553Z 
2026-09-06T13:09:02.1244654Z ### REQ-TEST-TMPDIR-HYGIENE
2026-09-06T13:09:02.1252161Z - Title: Windows test fixtures MUST NOT leak their temp dirs — find and fix the leak that accumulated 14,319 `.tmp*` dirs (163 GB) in %TEMP% on hfenduleam over 2026-07-19→26 and ate the box to 0.00 GB free during the v0.44.0 cut (find credit: deployah). The class presents as resource exhaustion wearing a timing-flake mask: the release-blocking red was spt-daemon::sync `two_tier_sync_lands_and_gate_refuses_server_side` dying 'No space left on device' INSIDE its own leaked-class tmpdir, and the PRIOR red (digest_cross_node brain-IPC deadline elapse) was the SAME root through a timing probe — burns release windows and invites false environmental discharges (deployah self-corrected his own). ROOT IS UNPROVEN AT MINT (investigate, don't assume): tempfile-crate dirs self-clean on Drop, so something defeats Drop — candidates from the incident record: process killed mid-drop (fixture children force-killed while owning the dir), detached/leaked children pinning the dir cwd so removal fails, panic paths that never unwind. CANDIDATE ROOT PROMOTED TO PROVEN (hertz, doyle ruling 2026-08-02, releases#110 measurement): the cwd-pin leg is no longer a hypothesis. Positive control on HFENDULEAM — spawn a child with cwd = a scratch dir, then attempt removal: writes INSIDE the dir succeed while delete is BLOCKED (the asymmetry that makes it read as neither a file lock nor a rig failure), and the dir deletes first try the instant the holder dies. The holder in that control was an orphaned GRANDCHILD (`PING.exe`, cwd inherited through `cmd`) that survived its parent's kill, so the defeating mechanism reaches deeper than the direct children this entry names. Corollary that changes where to look: the conhost is NOT the holder (its cwd is OS-assigned — a box-wide census returned 43 readable conhosts, all under `C:\WINDOWS`, zero under any tree), so a leaked-tmpdir investigation that clears conhosts and declares the path clean has proven nothing. Mechanism and tooling: KNOWN-HAZARDS 5.18 / REQ-HAZARD-TREE-CWD-PIN plus `.github/ci/find-cwd-holders.ps1`, which names the holder of any given directory. Deliverable: name the leaking fixture path(s) with evidence, fix the cleanup (guard/finalizer that survives the kill path, or a fixture-scoped reaper), and prove it with a before/after dir-count on the suite. Second recorded hfenduleam disk-full-CI instance.
2026-09-06T13:09:02.1252442Z - Required stages: impl, unit
2026-09-06T13:09:02.1252471Z 
2026-09-06T13:09:02.1252584Z ### REQ-CI-FREE-SPACE-PREFLIGHT
2026-09-06T13:09:02.1264469Z - Title: Every CI job on a shared self-hosted runner FAILS LOUD AND FIRST on insufficient free disk, never obscurely on a downstream timing/space symptom: a preflight step asserts a free-space floor on the workspace drive and fails naming the RESOURCE (free bytes, floor, drive) so the next exhaustion costs one clear red instead of a release window of mystery flakes (proposer: deployah, during the v0.44.0 disk-exhaustion incident — the rule-failed-its-author-needs-a-mechanism shape: 'check disk first' was already a memory rule and still cost two CI windows because it lived in judgement, not mechanism). Floor calibration honest at mint: the observed incident burned tens of GB per suite run (leaked fixture dirs + build targets); pick a floor from measured suite consumption plus headroom and RECORD the derivation in the workflow comment — an unexplained magic number invites drift. AMENDED 2026-08-03 (hertz RCA of golden 30860770146, doyle-ruled option (i) the same day): 'FIRST' now means first among EXPENSIVE steps, not literally step 1, in the two two-host jobs — there the floor is asserted AFTER `actions/checkout`, because checkout runs `git clean -ffdx` and that IS the reclaim. Measured: the Windows test leg materialises ~50 GiB of gitignored `target/` (29,777 of 30,022 files written inside that job's own window), the next job's checkout deletes it, and twohost-a starts seconds later — so the pre-checkout read is taken at the disk's maximum. 30860770146 died exactly there: RESOURCE red at 24.4 GiB free, no test run, over bytes its own next step was about to reclaim; the reorder alone would have turned that red green with zero reclaim. The protective purpose is intact — checkout is seconds and every build is still downstream — but the ordering is now a stated exception rather than an accident. EXTENDED 2026-08-29 to the n1-gate job (deployah, on the v0.66.0 SEMAPHORE cut; doyle informed with the measurement): n1-gate had kept the literal-first shape and hit the identical trap twice in one cut. Measured on golden 33273378238 attempt 2, hfenduleam: n1-gate asserted PRE-checkout at 21:58:36 and read 32,219,230,208 free against the 34,359,738,368 floor and failed in 9 seconds with checkout SKIPPED, while twohost-a on the SAME BOX asserted POST-checkout at 21:59:15 and read 104,530,997,248 and passed — 39 seconds apart, ~72 GB reclaimed by the intervening `git clean -ffdx`, opposite outcomes decided solely by read order. Two pre-checkout low-water readings 64 minutes apart (r2 20:54:36, r3 21:58:36) agree within 10 MB, so the adjacency is deterministic, not a flake, and a bounded WAIT placed before checkout cannot fix it by construction — the reclaim it would wait for is performed by its own next step. WIDENED the same day to the `test` job (doyle ruling on the scope note this entry carried): that latent-shape note is DISCHARGED, not deleted — leaving the amendment clause armed on one job is precisely how the trap survived 26 days to cost a cut two golden attempts. Gate applied before moving it: the `test` floor was verified to carry NO checkout-protective purpose — its comment derives the 32 GiB from the observed full-suite footprint plus headroom for one complete run, so it guards the SUITE, not checkout. The checkout-protective steps in that job are the two reap guards (Windows exe-lock, Linux flood orphans), whose own comments state they must precede checkout because the post-checkout guard is too late for the clean; they STAY pre-checkout and only the floor moved. EXTENDED 2026-09-06 (WEBSERVE floor trio IR-46/IR-59/IR-73): all eight ci.yml/release.yml floor sites are post-checkout, having no checkout-protective purpose. START and always-run END DISK assertions record raw free bytes, the unchanged 32 GiB floor, job/runner identity and PASS or RED, labelled INSTANT; END runs before teardown even after workload failure. Golden test and n1-gate carry the same readings, with a fresh assertion immediately before docs-drift. These samples are not a run minimum or a headroom guarantee. Raising the floor lacks measured peak evidence; continuous sampling is not part of this remedy.
2026-09-06T13:09:02.1264882Z - Required stages: int
2026-09-06T13:09:02.1264916Z 
2026-09-06T13:09:02.1265006Z ### REQ-PID-ROLE-EVIDENCE
2026-09-06T13:09:02.1274397Z - Title: THE RECORDED PID MUST CARRY WHAT IT MEANS. `info.pid` has two incompatible meanings decided by write path, and every reader has been guessing: the `api listen` path records a process that GENUINELY HOLDS the endpoint (its death IS the endpoint's relay death), while the `api bind` path records the announcing CLI, which is EXPECTED TO EXIT within seconds and whose death means NOTHING — the hosting life it announced (a broker PTY session) is not recorded anywhere in the row. LIVE MEASUREMENT (todlando, HFENDULEAM 2026-07-27, the finding that forced this mint): todlando pid 22588 DEAD, doyle pid 45160 DEAD, deployah pid 29176 DEAD — all controllable=true, state=live_agent, status=online, and all three GENUINELY ALIVE AND WORKING (the measurement was taken by one of them, messaging another) — against hertz pid 11216 ALIVE and mobile-gw pid 46152 ALIVE, which are real `api listen` relays. The split is by adapter integration pattern (claude-spt binds and exits; omp-spt/mobile hold a listener), NOT by anything a reader can see in the record. CONSEQUENCE ALREADY PAID: two proposed fixes for the emphasys convergence gap were BOTH falsified pre-build on this fact — oracle-first convergence, and carry-forward scoped to 'earning pid alive' — each would have converged three live agents node-wide. Both were keyed on a pid whose meaning they could not read. FIX: stamp the role at the seam that writes the pid — `relay` on the listen path, `binder` on every bind path — never inferred at read time. Absent ⇒ legacy row ⇒ NO KNOWLEDGE, and every consumer fails toward alive (inheritance stands, convergence never fires); such rows heal at their next re-bind. Consumers re-key on it: relay-death convergence fires only on `relay`, and the controllable carry-forward drops a stamp only when a prior `relay` pid is provably Gone. CLASS: a claim keyed on the wrong thing (kin: `is_perch_alive` reading a status FIELD as hosting topology; the inherited capability stamp routing a liveness proof) — the cure is to make the record SAY the thing rather than have readers infer it. AUDIT RIDER (doyle, required in the doc stage since convergence re-keys on this field): enumerate which row classes reach the convergence branch under the new key — fresh NonAgent/None rows, shell instances, gateway listens — and pin the answer structurally, not just for tonight's node. Gate: doc — the record-shape doc carries the field, the two meanings it ends, the legacy/no-knowledge rule, and the audit-rider enumeration; impl — the field on InfoJson stamped from the entry path at the bind seam, plus both consumer re-keys; unit — the role-stamp table over the three hosting authorities, the carry-forward table incl. the claude-spt-shaped NEGATIVE (binder-role dead pid + Some(true) + listener re-bind ⇒ inheritance STANDS, row never routes to convergence), and the convergence role gate incl. the legacy-absent row; int — the synthetic emphasys template (relay-role + dead pid/parent + valid birth stamp + listener-only re-bind ⇒ derives fresh ⇒ routes to convergence ⇒ oracle Gone ⇒ converged) with a BrokerPty sibling that re-asserts fresh and is untouched.
2026-09-06T13:09:02.1274873Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.1274906Z 
2026-09-06T13:09:02.1275019Z ### REQ-RELAY-DEATH-CONVERGENCE
2026-09-06T13:09:02.1281620Z - Title: A dead harness relay MUST converge the endpoint's projection — an endpoint whose relay process is provably gone cannot keep rendering `ONLINE - HARNESS ONLY` with ready=true alive=true and a registered address merely because the owner parent is still alive. RE-PROVEN PRESENT on 0.44.0 (hertz W0 item 2, 2026-07-27, fresh isolated build sha-prefix 00e15f): relay pid 38764 killed 06:21:01Z, owner pid 12156 live; 148s later info.json still pid=38764 status=online, list JSON ready=true alive=true address=127.0.0.1:57709, TCP to that address unreachable, human list `ONLINE - HARNESS ONLY`. Sibling probes: live-relay sibling's socket reachable and `send` -> SENT with the EVENT received (the rig CAN see delivery); clean `endpoint stop` sibling rendered status=offline ready=false alive=false (the rig CAN see convergence). FIX SHAPE: relay-death convergence — a dead/unreachable relay clears the ready/address/liveness projection for its endpoint within a bounded window, in ONE authority: the verdict MUST route through the shipped custody/process-identity predicate (KH 2.5 — liveness authority lives in one resolver, never re-derived beside it; Unproven never kills). Kin: the Athenaeum stale-ALIVE generator (HOSTING_AUTHORITY_DEMOTED demote-to-no-claim arm, REQ-LISTEN-PRESERVES-HOSTING-TOPOLOGY — the demote-vs-offline fork needs the same identity predicate), SHELL-STALE-ONLINE (same lying-signal class, shell flavor), REQ-MSG-INJECT-LEG-DROP-VISIBLE (the observability face of the same wedge). TITLE ADDENDUM 2026-07-27 (doyle ruling, todlando build): THE VERDICT FIRES ONLY WHERE THE RECORDED PID WAS A **HOLDER** (pid_role=relay, REQ-PID-ROLE-EVIDENCE). The shipped branch asks the oracle about whatever pid the record carries, and `info.pid` means two different things by write path — a holding `api listen` relay, or an `api bind` announcer that exits within seconds. Measured on HFENDULEAM 2026-07-27: todlando 22588, doyle 45160, deployah 29176 all DEAD-and-online (binder pids) against hertz 11216 / mobile-gw 46152 alive (relay pids), so an unrole-gated verdict would have offlined three live agents. A row with no role stamp (legacy) is NO KNOWLEDGE and never converges; it heals at its next re-bind.
2026-09-06T13:09:02.1281911Z - Required stages: impl, unit, int
2026-09-06T13:09:02.1281939Z 
2026-09-06T13:09:02.1282039Z ### REQ-CREATE-BIND-REST-ACTIVE
2026-09-06T13:09:02.1287066Z - Title: A successful fresh `endpoint run --create` bind MUST normalize rest intent to active IN THE SAME lifecycle transition — it must not leave `rest_state=suspended` from the prior life, and must never require a separate explicit `endpoint wake` to make a freshly-bound-online endpoint's rest vocabulary truthful. RE-PROVEN PRESENT on 0.44.0 (hertz W0 item 3, 2026-07-27, isolated build): `endpoint stop` -> raw status=offline rest_state=suspended session_id=5c93...; successful `run --create --start` minted session 714260b31d440df3 status=online — rest_state STILL suspended; independent repeat matched (new session 595f..., same sticky suspended). Sibling probes: fresh create with no prior perch carries NO rest_state field (the field itself is inherited residue, not a fresh stamp); explicit `endpoint wake` on the reproduced row flips suspended->active (the transition exists and works — it is simply never taken by the create/bind path). Downstream symptom from the original F-034 filing: `endpoint suspend` NO_EDGEs on a visibly ONLINE endpoint because the recorded rest state already claims suspended. FIX SHAPE: the create/bind success path stamps rest intent active atomically with the online transition (terminal-normalize family — same transition, never a follow-up repair step); deliberate suspend-while-online semantics (ADR'd rest vocabulary) must be preserved — the normalization applies to FRESH binds inheriting a dead life's rest state, not to a live session an operator suspended. Kin: KH 7.49 (a verb never stamps a state it did not cause — this is the inverse: a transition failing to stamp the state it DID cause), terminal-normalize work 0.41-0.43 (which closed the adjacent shapes but never reached this seam).
2026-09-06T13:09:02.1287243Z - Required stages: impl, unit, int
2026-09-06T13:09:02.1287281Z 
2026-09-06T13:09:02.1287381Z ### REQ-ACL-SURFACE-VOCAB
2026-09-06T13:09:02.1292447Z - Title: Access control is granular at the CONTROL SURFACE, not at the endpoint. A rule is (target endpoint x surface x subject) -> allow/deny, where a surface is a named remote-reachable operation class with an OPEN CONSTANT_CASE string vocabulary — new surfaces mint ids without a schema change, and an unrecognized surface string in a rule is legal (it governs nothing until that surface is built). The v1 set is the nine ratified ids: MSG, RC_VIEW, RC_ATTACH, DIGEST, WAKE, SUSPEND, XFER, SHELL_LINK, DISCOVER. WHAT THIS ENDS: the ADR-0009 whitelist was all-or-nothing per endpoint — admitting a node for messages also admitted it to drive the terminal, pull digests, and transfer files, because one access_check covered every wire-inbound family at once. On a SHARED SUBNET (member nodes belonging to different human operators, the gated adversary being agents) that coupling is the whole problem: there is no way to publish a view without handing over the keyboard. Each of the daemon's gate call-site families now tags itself with its surface, and the two families carrying a request-shaped distinction split: attach on AttachIntent (Viewer -> RC_VIEW vs Control/Take -> RC_ATTACH, see REQ-ACL-RC-VIEW-SPLIT), rest on the rest event (Wake -> WAKE vs Suspend -> SUSPEND). Gate: doc — the CONTEXT.md control-surface glossary entry; impl — the spt_store::access::surface vocabulary module, access_check's surface parameter, and the six call-site families tagging themselves; unit — the vocabulary shape (nine ids, CONSTANT_CASE, open to unminted strings) plus a per-surface decision table proving a grant on one surface does not admit the same node on another.
2026-09-06T13:09:02.1292715Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1292743Z 
2026-09-06T13:09:02.1292838Z ### REQ-ACL-SUBJECT-CHAIN
2026-09-06T13:09:02.1303708Z - Title: One FIRST-MATCH-WINS subject chain decides every access question, with an implicit-open bottom that preserves current fleet behavior. Order: per-endpoint sender-endpoint rule -> per-endpoint node rule -> per-endpoint subnet-wildcard rule -> node-scope node rule -> node-scope subnet-wildcard rule -> endpoint mode for the surface -> node mode for the surface -> join-time-captured subnet mode for the surface -> (nothing matched) OPEN. The stateful-firewall reply exemption and the same-node loopback Allow PRECEDE the chain and are not tiers — a degraded or locked-down node keeps its own conversations alive. Modes are the per-surface default posture for unlisted subjects (open/closed), settable at endpoint and node scope; the captured-subnet-mode table ships as a SCHEMA STUB in W1 (W2's join capture populates it — absent means the chain bottoms out open). Store schema v2 carries subject-kind entries at both scopes, migrates every v1 AccessEntry.nodes row into an equivalent (endpoint mode closed + one all-surface node Allow per listed node, so a listed node keeps access and an unlisted one loses it exactly as before), and DROPS the inert users field with the no-user-identity ruling (a shared subnet's trust unit is the node as human-proxy; 'user' is deliberately not a security identity). TIER 1 IS SCHEMA-REAL BUT INERT IN W1 (doyle ruling 2026-07-29, on todlando's pre-build finding): the design-of-record premise 'sender identity is daemon-stamped and nodes are trusted' is FALSE of its first clause today. The only wire record carrying a sender endpoint is WanMessage.from, which KNOWN-HAZARDS 7.5 (REQ-HAZARD-WAN-ORIGIN-AUTH) binds verbatim as reply-routing metadata 'never an authorization subject' — and it is caller-supplied (spt send --from; cli.rs resolve_from lets the explicit arg win over session detection), so keying a rule on it would admit a FORGED subject, the exact failure 7.5 exists to prevent. Precedent already ruled in-tree: REQ-MSG-5's classify_local_origin faces the same question and answers it with the session-proven perch id, noting '--from alone never confers an agent identity'. Independently, five of the six gate families (attach, xfer, rest, shell-link, digest) carry no sender endpoint on the wire in any form. So the kind persists, matches, and round-trips, but every W1 call site supplies None. Lighting it up needs an authenticated daemon-stamped sender field — a NEW additive field, never a repurposing of from — which is its own wave and its own REQ. Gate: doc — the CONTEXT.md endpoint-access-whitelist entry, whose false daemon-stamped premise this change corrects in the same diff, plus the control-surface-modes entry carrying the chain; impl — spt_store::access schema v2 (Subject/AccessRule/Modes/EndpointAcl/NodeAcl/CapturedSubnetModes), AccessStore::decide, the v1 migration, and the daemon gate threading origin subnet membership in; unit — the chain proven tier by tier (each tier decides only when every tier above abstains, bottom is open), the v1 verbs' polarity (allow restricts, revoke narrows without widening, open is the only widening), v1->v2 migration semantics, v2 round-trip of every construct, and TWO inertness guards — a store-level one proving the kind matches when fed and abstains when not, and a gate-level one proving no call site feeds it (that guard goes red on purpose if a later change threads a sender identity into the gate).
2026-09-06T13:09:02.1304085Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1304113Z 
2026-09-06T13:09:02.1304210Z ### REQ-ACL-FAIL-CLOSED
2026-09-06T13:09:02.1312101Z - Title: A corrupt or unreadable access store degrades CLOSED, loudly (ADR-0053 — an ADR-level flip of ADR-0009's deliberate fail-open ruling). ADR-0009 justified fail-open explicitly: the whitelist was 'a same-subnet convenience boundary layered inside subnet membership, not the system's outer wall', because every member node was the one user's own machine. The SHARED SUBNET ruling breaks that premise — member nodes now belong to different human operators and the access layer gates agents — and a boundary that evaporates when its store file corrupts is not a boundary an operator can reason about. So a store that CANNOT BE READ refuses unlisted-subject traffic on every surface rather than admitting it, and says so on the refusing node naming the store path (silence plus refusal reads as network failure and burns diagnosis time). TWO THINGS SURVIVE THE DEGRADE: the stateful-firewall reply exemption (correlation state is a separate file keyed on the endpoint's own recent outbound — a degraded node keeps its own conversations alive) and the same-node loopback Allow. THE SPLIT THAT MAKES THIS SHIPPABLE: an ABSENT store file is NOT a degrade. It is the unconfigured baseline — every fleet node today has no access.json, and reading absence as closed would refuse all unsolicited cross-node traffic fleet-wide on upgrade. The two cases split on std::io::ErrorKind::NotFound and nothing else: never-created reads as an empty store whose chain bottoms out open; any other io error or a parse failure is a degrade. Absent-is-open keeps an unconfigured fleet reachable, unreadable-is-closed makes the boundary real. Consequence accepted per ADR-0053: a personal fleet with zero configured rules now blocks unsolicited cross-node traffic if its store file CORRUPTS — the failure is loud, local, and repairable, and the alternative (silent admission on a shared subnet) is the worse lie. Existing fail-open tests are repinned to the new contract as part of this diff (a product contract change, not a test fixup). Gate: doc — ADR-0053 and the CONTEXT.md control-surface-modes entry carrying the degrade rule; impl — AccessStore::load_checked/load_checked_from returning AccessDegraded (path + cause) with the NotFound split, and the daemon gate's refuse-plus-diagnostic arm ordered AFTER the reply exemption; unit — the degrade split (absent is baseline, corrupt and unreadable both degrade, diagnostic names the path) and the gate-level proof that the same call is allowed before corruption, refused on every surface during it, still allowed as a reply throughout, and allowed again once the store is repaired without a restart.
2026-09-06T13:09:02.1312277Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1312314Z 
2026-09-06T13:09:02.1312413Z ### REQ-ACL-RC-VIEW-SPLIT
2026-09-06T13:09:02.1315455Z - Title: Watching an endpoint's terminal and DRIVING it are separately grantable: attach gates on the request's AttachIntent — Viewer -> RC_VIEW, Control/Take -> RC_ATTACH. Before this, one access_check(endpoint, origin, Unsolicited) covered every attach intent, so admitting a node to view an endpoint necessarily admitted it to take the keyboard (and, with Take, to displace an incumbent controller). On a shared subnet that is the difference between showing a colleague's agent what happened and letting their agent drive yours. The split is keyed on the intent the REQUEST carries, evaluated at the serve side under the handshake-proven origin — never on anything the attaching side can restate after the gate. Kin: ADR-0042 (rc-attach truth) and REQ-ACL-SURFACE-VOCAB, which mints the two ids. Gate: doc — the CONTEXT.md control-surface entry naming RC_VIEW and RC_ATTACH as distinct v1 surfaces; impl — the attach_surface mapping and attach.rs gating through it; unit — the mapping over all three intents plus a decision table proving an RC_VIEW grant does not admit RC_ATTACH.
2026-09-06T13:09:02.1315688Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1315718Z 
2026-09-06T13:09:02.1315808Z ### REQ-ACL-DISCOVER-GATE
2026-09-06T13:09:02.1319805Z - Title: DISCOVER is a real gated surface: registry resolve/advertise and the resources blurb are filtered per viewer, so discovery leaks nothing a viewer could not reach. ADR-0009 NAMED this leg — 'discovery (resource advertisement) is gated by the first two gates' — and CONTEXT.md's resource-advertisement entry promises that an endpoint whose access rules exclude the viewer's node by naming DISCOVER in a deny never appears in that view; a legacy grant plus blanket-closed posture does not imply that exclusion because DISCOVER is default-on. Only the VISIBILITY half was ever built; the access half has been an unbuilt promise since M4. Un-gated discovery on a shared subnet is an inventory of every agent on every member node, including the ones an operator deliberately walled off — and a resources blurb is a yellow-pages description written to attract exactly the collaboration the access rules refuse. Gate: doc — the CONTEXT.md resource-advertisement entry (the promise this closes) and the control-surface entry minting the id; impl — the DISCOVER check on the registry resolve/advertise path plus per-viewer filtering of the resources blurb; unit — a viewer denied by a DISCOVER-naming rule sees neither the endpoint row nor its blurb, while a viewer allowed DISCOVER sees both, legacy blanket closure alone remains disclosed, and the filter is per-viewer rather than global.
2026-09-06T13:09:02.1319967Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1319995Z 
2026-09-06T13:09:02.1320099Z ### REQ-ACL-DISCOVER-DEFAULT-ON
2026-09-06T13:09:02.1327702Z - Title: DISCOVER IS ON BY DEFAULT: a `closed` posture at ANY mode tier -- endpoint mode, node mode, or the join-time-captured subnet mode -- no longer implies a DISCOVER deny, and the ONLY thing that closes DISCOVER is a deny that NAMES it: an access rule row, or a `per_surface` mode entry (engine-room-set at the node tier). Operator-ruled 2026-08-17 (releases#180/#181): being findable is what makes a knock -- the ask to be admitted -- possible at all, so a node that closed its posture to say 'do not talk to me' had also silently said 'and you may not ask', which was never the choice being made; the messaging-first majority pays for a discovery decision they never took. THE DEFAULT LIVES IN THE SURFACE VOCABULARY, NOT BESIDE THE CHAIN: the `surface::TABLE` row grows a `default_on` discriminant (single-source, the shape `attributable` already established) and exactly ONE method reads it -- `Modes::for_surface_governing`, which for a default-on surface resolves `per_surface` ALONE (exact key then case-fold, the releases#66 precedence rule VERBATIM) and declines the `self.all` blanket fallback. `Modes::for_surface` is untouched, so every reader that asks 'what posture is set here' keeps its present meaning; only the readers that DECIDE change. ALL THREE MODE TIERS AND `effective_posture` ROUTE THROUGH IT: the posture reading is deliberately the chain's own tiers in the chain's own order, so leaving it on the blanket would put two accounts of one fact out of agreement -- the exact failure that method exists to prevent -- and the `may_grant_node_subject` consequence is CONSISTENT rather than a hole, since post-change a node-subject DISCOVER allow admits nothing the default already does not. `MatchedTier::SurfaceDefaultOpen` is minted: a pass by surface default and a pass by chain bottom are different facts, needed by the ER/DISCOVER conjunction notice and the trust warning's pass-reason, and cheap only at mint. WHAT DOES NOT MOVE: ADR-0053's degrade (a store that cannot be read still refuses DISCOVER outright -- the default is a posture carve-out, never a licence to advertise policy we cannot read), REQ-ER-NOT-ADVERTISED's pre-chain engine-room carve-out (posture beats whitelist), and the redemption path, which is DISCOVER-free by design and node-tier-routed. Gate: doc -- the CONTEXT.md mode-tier entry stating the carve-out as ratified vocabulary, naming the one off-switch and the three things that do not move; impl -- the table discriminant, its single table-driven reader, the governing resolver, the three tiers, `effective_posture`, and the new tier; unit -- a blanket-closed node admits DISCOVER and STILL refuses MSG in one cell, an explicit DISCOVER-naming mode closes it at each of the three tiers SEPARATELY, a DISCOVER-naming deny ROW still denies, the case-folded key is honoured ON THE NEW PATH with exact-match precedence intact, the chain and `effective_posture` agree in both directions, the default-open pass is distinguishable from the implicit-open bottom, the reader is proven a function OF THE TABLE against a fixture the shipped table cannot express, and a degraded store refuses DISCOVER beside a readable blanket-closed one that admits it.
2026-09-06T13:09:02.1327968Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.1327996Z 
2026-09-06T13:09:02.1328118Z ### REQ-ACL-FORK-WITHOUT-DISCOVER-CONSEQUENCE
2026-09-06T13:09:02.1336575Z - Title: A `FORK` GRANT WHOSE SUBJECT CANNOT RESOLVE THE ENDPOINT SAYS SO AT WRITE TIME -- a stated consequence, never a refusal: the rule lands exactly as typed and the operator is told that as written the subject cannot resolve the endpoint it may now fork, so the attempt fails later as an unresolvable subject. Forking a remote endpoint takes TWO surfaces and only one is obvious: `FORK` authorizes the operation, `DISCOVER` is what lets the grantee resolve the row at all. Field origin (releases#76, from the releases#29 cross-node rung): node B granted node A `FORK` alone, A could never fork, and granting the pair fixed it -- diagnosed empirically, not by reading. The operator-facing defect was never that the pair is required; it is that the requirement was INVISIBLE, since the write succeeds and reads back as typed. THE CONDITION IS THE CHAIN'S OWN `DISCOVER` VERDICT FOR THE SUBJECT, NEVER RULE-ROW PRESENCE, and the re-derivation matters because #76's own filed premise stopped being true inside this milestone: the filing reasoned that per-surface isolation closes every unlisted surface so a FORK-only grant DENIES DISCOVER, but read against the code a FORK rule simply does not COVER DISCOVER (`AccessRule::covers`), so DISCOVER fell THROUGH to the mode tiers and it was the blanket-closed posture there that refused it -- exactly what REQ-ACL-DISCOVER-DEFAULT-ON removed. A row-scan condition would therefore now be wrong in BOTH directions: silent on the explicit deny that still bites, loud on the blanket close that no longer does. ONE VERDICT, TWO CONSUMERS: the same predicate answers this write-time consequence and REQ-KNOCK-PRESCRIBES-FORK-PAIR, asked of one function rather than described twice -- this file already records what a second copy of an approval classifier cost. SUBJECT SHAPES ARE ANSWERED HONESTLY OR NOT AT ALL: a NODE subject is exact; a SUBNET WILDCARD is one rule with a verdict per machine, so the refused members are NAMED (with the count this node can SEE stated beside them, because a roster is this node's view and not the subnet's truth); a SENDER-ENDPOINT subject is deliberately NOT answered, since `FORK` carries no proven sender and such a rule matches no traffic at all -- its real defect is a different sentence owned by the attributability surface, and adding 'it could not resolve either' would rank a second problem above the first. A blanket rule (empty surface list) covers `DISCOVER` by definition and is never a gap; a DENY is never a gap. Gate: doc -- the CONTEXT.md entry stating the pair, the verdict-not-rows condition and the re-derivation that makes it necessary; impl -- the shared verdict function over a caller-held roster/subnet snapshot, the gap classifier, the consequence line, and the accept-site call; unit -- a FORK-only allow to a DISCOVER-denied node states the consequence, the same grant to a node that CAN resolve is silent (the earned silence), a FORK+DISCOVER grant is silent, a blanket grant is silent, a deny is silent, a subnet subject names the refused members apart from the seen count, and a sender-endpoint subject is silent by ruling rather than by accident.
2026-09-06T13:09:02.1336918Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.1336952Z 
2026-09-06T13:09:02.1337061Z ### REQ-KNOCK-PRESCRIBES-FORK-PAIR
2026-09-06T13:09:02.1342324Z - Title: THE KNOCK PRESCRIPTION PRESCRIBES THE PAIR WHERE THE PAIR IS NEEDED: when a pending knock asks for `FORK` without `DISCOVER` and the knocker's machine would still be refused `DISCOVER` on the target, `knock list` prescribes `spt knock approve <id> --surfaces FORK,DISCOVER` beside the form line, naming why -- the knocker would be granted an operation it cannot reach. Prescribed ONLY where the binary would otherwise refuse (doyle Q3), from the SAME predicate REQ-ACL-FORK-WITHOUT-DISCOVER-CONSEQUENCE answers, so the two statements about one question cannot drift; the rule an as-requested approval WOULD write is BUILT and handed to that predicate rather than described, which is the shape `approval_form` already established after a hand-written second copy of an approval classifier went stale in the field. IT RIDES BESIDE THE FORM LINE, NEVER REPLACING IT: the form line says what the SEAT must pass for the approval to be honored, this says what the approval must GRANT to be usable, and an operator who pasted the form line and stopped would write the exact grant that filed the request. `--approve-requested` IS NOT TAUGHT TO WIDEN: it means exactly what was asked for, and quietly granting a surface nobody requested would make that flag a liar at every other site that uses it -- so the prescription is an explicit `--surfaces` invocation the operator can read before pasting. Silent when the request already names `DISCOVER`, when it is blanket (which covers every surface), when it names no `FORK`, or when the knocker's machine can already resolve the target. Gate: doc -- the CONTEXT.md fork-pair entry, prescription half; impl -- the prescription composer over the built rule and the shared verdict, and its `knock list` call site below the form line; unit -- a FORK-only knock from a machine that cannot resolve gets the paired invocation with DISCOVER appended, the same knock from a machine that CAN resolve gets nothing, a FORK+DISCOVER request gets nothing, a blanket request gets nothing, and the composed command parses as the approve verb's own `--surfaces` form.
2026-09-06T13:09:02.1342504Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.1342533Z 
2026-09-06T13:09:02.1342639Z ### REQ-KNOCK-NOTIFY-INSTIGATOR
2026-09-06T13:09:02.1353909Z - Title: THE THREE CONSENTED KNOCK COURTESIES REACH THE PARTY THEY ARE OWED TO -- an approval notifies the KNOCKER, a redemption notifies the code's MINTER, and each is ADDRESSED to that endpoint rather than aimed at whoever the user was last seen at. Field origin (releases#149/#169): lia minted an invite code, it was redeemed, and lia was never told; the notification existed, landed on the right NODE, and could not surface. THREE STRUCTURAL FAULTS IN ONE SHAPE, written the same way at all four producing sites (local approve, cross-node approve, local redeem, cross-node redeem): the recipient rode the `from_id` slot because the row had NO ADDRESSEE FIELD, so the notif said the recipient issued it; the subnet was the empty string, which is not a member subnet, and BOTH surfacing paths walk member subnets only -- `resurface_at_boundary` iterates `policy.subnets` (built from real memberships) and first-fire admits candidates through `policy.visible(id, subnet)`, false for every endpoint at `""` -- so the row was unreachable by construction; and they called `NotifStore::produce_scoped` DIRECTLY, bypassing the daemon's produce-and-fire front door, so nothing was delivered live either. THE FIX IS AN ADDRESSED NOTIF, NOT A LOUDER MISS (doyle Q1, ADR-0046 amendment 2): `NotifRow` grows an optional `to_id` (additive serde, the `SubscribeReq.code` precedent -- an N-1 peer parses an addressed row clean and reads it as unaddressed), honored at BOTH surfacing paths -- first-fire targets the ADDRESSEE and the address BEATS most-recently-active, with NO fallback to the MRA winner (an absent addressee yields NoTarget and the row waits for that endpoint's own next boundary), and a boundary resurface at any OTHER endpoint SKIPS the row with its own outcome rather than a suppression, because `not you` and `not now` are different facts. Merely giving the rows a real subnet was REJECTED as a fix: delivery would stay presence-MRA, so on a 14-endpoint node the approval notice still lands on a bystander. Re-homing the three onto the message spool was rejected too -- they were deliberately put on the notif spool against inbox delivery by the ratified W4 decision, and the spool would silently retire it. ONE PRODUCER FOR ALL FOUR SITES, with a REAL member subnet: the correlation subnet the exchange rode (the member subnet whose registry has seen the peer's node) where one is derivable, else the first member subnet the ADDRESSEE IS VISIBLE IN -- visibility is the condition both surfacing paths impose, so a subnet the recipient is hidden in would reproduce the unreachable row under a nicer name. THE APPROVAL COURTESY IS LIFTED AHEAD OF THE MUTUAL ARM: the cross-node producer sat after the `ReceiptOutcome::Consumed` arm, which only a `--mutual` knocker reaches, so a plain knock's receipt returned `Ignored` and the ORDINARY knocker -- the party who asked, with no other way to learn the answer -- was structurally un-notifiable. It is now produced on every APPROVAL, proven by the outbound knock row THIS node sent (matched on correlation id, the answering endpoint and the handshake-proven origin node) rather than by the pre-authorization, so a receipt matching none of our knocks notifies nobody and a guessed correlation id cannot become a spray channel; a DENIAL still notifies no one, since the ratified set does not extend to it. A REPLAYABLE arrival carries a coalesce key naming the EXCHANGE (`spt-core:knock-answered:<correlation-id>`) and not the kind -- two approvals are two facts and both stand, while two receipts for one knock are one fact said twice, so ADR-0046 decision 2's supersession leaves exactly one LIVE courtesy per knock instead of nagging. Gate: doc -- ADR-0046 amendment 2 stating the addressee, both honoring paths, the member-subnet rule and the lift; impl -- the `to_id` field, column and addressed produce front doors, the addressed first-fire target and the resurface skip, the shared courtesy producer and its subnet chooser, the outbound-knock proof seam, and all four producing sites moved onto it; unit -- per courtesy produce->fire->surface, an addressed row NEVER lands on a bystander (the 14-endpoints-on-a-box case), an absent addressee yields NoTarget rather than the MRA winner, a resurface elsewhere skips and the addressee's own boundary surfaces it, a plain (non-mutual) approval receipt produces the courtesy, a receipt matching no outbound knock produces nothing, and the subnet chooser refuses a subnet the addressee is hidden in.
2026-09-06T13:09:02.1354224Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.1354252Z 
2026-09-06T13:09:02.1354370Z ### REQ-ACL-ER-DISCOVER-CONJUNCTION-NOTICE
2026-09-06T13:09:02.1363711Z - Title: AN ACCEPTED RULE THAT CANNOT CURRENTLY TAKE EFFECT SAYS SO: an endpoint-scoped access mutation naming the ENGINE ROOM is accepted and, when its `DISCOVER` half cannot presently disclose anything, the accept site prints a LOUD CONJUNCTION NOTICE naming the engine room's advertisement whitelist as the other lever and its current posture. Disclosure of the engine room is an AND across two records: `DiscoverGate` checks the ER advertisement filter BEFORE the access chain (REQ-ER-NOT-ADVERTISED, ratified and unchanged by this requirement), so a rule accepted against an empty whitelist discloses NOTHING while reading as policy in force. Field case: ENLYZEAM's sole `DISCOVER` allow was ER-scoped `any-of SPT_DEV` with an empty whitelist, and every SPT_DEV peer saw only its own rows (hertz RCA 2026-08-05). THE RULED SHAPE (doyle, releases#163) IS ACCEPT-AND-TELL, and both halves of it are load-bearing: do NOT refuse the mutation -- the rule IS the conjunction's legitimate chain half and refusing it would make the only expressible half unexpressible -- and do NOT route it into the ER whitelist, because that record is the human/engine-room seat's authority and an AGENT-INVOCABLE VERB MUST NOT MUTATE IT. Kin doctrine: refused-rather-than-quietly-ignored (#5); the defect is DECLARE-SITE VS ENFORCE-SITE SILENCE, and acceptance is not the defect. WHICH HALF IS MISSING IS NAMED, not merely that one is: the whitelist names nobody; the whitelist does not name the machine this rule admits (checkable EXACTLY for a node subject, because the whitelist is node-keyed -- an advertisement lands on a peer NODE and every endpoint there reads it -- and deliberately NOT guessed for a subnet wildcard, which this site cannot enumerate into node keys, NOR for a sender-endpoint subject, which names no node at all: resolving that id to a machine would be a guess wearing a lookup, since the directory answers where this node last SAW that endpoint and not where the grant will be exercised from, so a notice built on it would name the wrong machine with full confidence. Both fall-throughs are the same non-guess doctrine and both are PINNED BY A CELL, because an unstated deliberate silence is indistinguishable from an unhandled arm to the next reader (doyle, #146 review); or the whitelist would disclose and the posture withholds ahead of it. GAPS ARE REPORTED WHITELIST-FIRST THOUGH THE GATE CHECKS POSTURE FIRST: posture is a state that changes the moment a controller attaches, the whitelist is the durable lever the operator must actually edit, and naming a transient blocker while a permanent one sits underneath sends them to fix the wrong thing. ONLY AN ALLOW IS CLASSIFIED -- a deny's `DISCOVER` half takes effect the moment it lands, so a notice there would teach that a refusal is conditional when it is not -- and a REMOVAL never reaches the notice by an explicit op guard rather than by the decision it carries, since a removal restates the tuple with the WIDENING polarity the gate wanted and not the rule's own. The record is read only when the target IS the engine room, so an ordinary rule mutation gains no file read. Gate: doc -- the CONTEXT.md entry stating the conjunction, the accept-and-tell shape, the whitelist-first order and what must not happen; impl -- the pure classifier over (target-is-ER, rule, whitelist, posture), the notice composer, and the accept-site call after the acceptance line; unit -- each gap arm classified apart, an ALLOW that the whitelist DOES name plus an online posture yields NO notice (the silence is earned, not merely unimplemented), a deny and a removal are silent, a non-ER target is silent, and the notice text names the whitelist as the other lever and the posture as its current state.
2026-09-06T13:09:02.1364021Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.1364055Z 
2026-09-06T13:09:02.1364164Z ### REQ-ACL-SURFACE-MODE-VERB
2026-09-06T13:09:02.1372712Z - Title: A NODE CAN CLOSE ONE SURFACE FROM ITS OWN ENGINE ROOM: `spt api access-node-surface-mode <SURFACE> <open|closed|unset> <id>` writes the NODE TIER's `per_surface` entry for exactly one control surface, and it is the ONLY off-switch a node owner has for a default-on surface. It exists because REQ-ACL-DISCOVER-DEFAULT-ON took the blanket posture out of a default-on surface's reach: after that change `access-node-mode closed` no longer closes DISCOVER, so the milestone's own promise -- a node owner who wants DISCOVER disabled can do so from their node's engine room -- is satisfied by this verb or by nothing (doyle 2026-08-17: shipping the carve-out without the off-switch is a greenlit-form deviation, and building it is the smaller act). THE NARROW TWIN, NOT A NEW AUTHORITY: it passes the SAME shared engine-room gate as `access-node-mode` (`api::engineroom::gate`, ADR-0052 decisions 5 and 6 -- one function, every engine-room verb through it, deliberately stated without a count) and needs NO empowerment, for the identical reason the blanket verb needs none: a node's own posture is its own business and the bring-up gate already proved membership. It lives in the `spt api` family rather than the rule-mutation family because that is where the gate, the auth proof and the ER capability vocabulary already are. NODE TIER ONLY: the endpoint tier's lever remains a deny row that names the surface, per-subject, until the concierge modernization mints its own form. THREE STATES, NOT TWO: `unset` REMOVES the entry, which is not a spelling of `open` -- an explicit open PINS the surface open against any later change of the vocabulary's default, so a two-state verb would make closing a default-on surface a one-way door whose only exit is a pin the writer never meant to author; the removal folds case for the reason every releases#66 reader does, since an entry stored as `discover` is the one that GOVERNS and a clear that missed it would report 'nothing was set'. THE PIN MUST BE VISIBLE (doyle's rider, 2026-08-17): an access view renders a pinned open distinguishably from a default open -- `DISCOVER open (pinned)` against `DISCOVER open (default)` -- because the same two words otherwise describe two different facts, and a blanket-closed summary that said only `closed` would read as closing DISCOVER too, which is exactly the reading the carve-out removed. The surface token is validated against `surface::TABLE` and canonicalized at the boundary: an unminted id stays legal in a RULE, where it governs only itself, and is REFUSED here, because this verb writes the key the chain READS and a typo would otherwise sit in the view as a posture that governs nothing. Gate: doc -- the CONTEXT.md mode-tier entry naming the verb as the node-tier off-switch and stating the three states; impl -- the store's node-tier per-surface setter, its case-folding clear and its read half, the api verb and its dispatch arm, the shared-gate call, the vocabulary refusal, and the pinned/default render in the access view; unit -- the verb writes only the named surface and leaves the blanket alone, `unset` restores the default rather than pinning open (with the default-on and ordinary cases asserted apart), a case-differing stored key is the one cleared, an unknown surface is refused with nothing written, a non-engine-room caller is refused by the shared gate, and the view renders pinned-open, default-open and closed as three distinguishable readings.
2026-09-06T13:09:02.1373127Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1373155Z 
2026-09-06T13:09:02.1373259Z ### REQ-UNLISTED-FOOTER-OMITTED
2026-09-06T13:09:02.1378563Z - Title: THE UNLISTED SECTION RENDERS ITS ROWS AND NOT AN ESSAY ABOUT THEM: `endpoint list` omits the two footer explainer lines under the UNLISTED heading -- the always-printed `evidence is what this node recorded; the presence word is their own node's answer, asked just now`, and the conditional `not asked -- no node is recorded for that row, so there was nobody to ask`. Operator-filed (releases#184, split from releases#149): both were confusing rather than helpful, and they were the only two lines in the family that explained the family to a reader who had already read the rows. WHAT STAYS, EXPLICITLY, because the deletion must not strand the section: the purple heading that NAMES what the section is, every per-row cell and its phrases -- the status word, the provenance label, the node label including the stated `node unknown`, the presence word, the `not asked` cell itself, and the evidence detail -- and the CONDITIONAL `unknown` legend, which the filing does not name and which explains a word an operator is about to act on that its own spelling does not carry (a relay-hosted peer cannot tell busy from offline). The retained legend's own doc comment is corrected in the same change from `the two legends below` to what remains, because a count that no longer matches its code is the stale-carried-forward sentence the next reader trusts. THE RETRACTION SWEEPS THE STRINGS, NOT JUST THE RENDER SITE (doyle Q5): the deleted phrases are hunted through the test estate so no assertion outlives the text it asserts -- an assertion on a deleted string is either a red or, worse, a green on a substring the ROW still carries. THE OMISSION IS PINNED BY A NEGATIVE CELL over a fixture that HAS an unasked row, since a fixture without one would pass on a footer that is merely unreached rather than gone. Gate: impl -- `render_unlisted` without the two lines and with its legend comment corrected; unit -- one rendered fixture carrying an unasked row asserts BOTH phrases absent while the heading, the row's `not asked` cell and the retained `unknown` legend are still present, so the cell separates the footer from the per-row vocabulary rather than merely counting lines.
2026-09-06T13:09:02.1378792Z - Required stages: impl, unit
2026-09-06T13:09:02.1378821Z 
2026-09-06T13:09:02.1378920Z ### REQ-SUBNET-DUAL-SEED-MINT
2026-09-06T13:09:02.1382531Z - Title: `subnet create` mints TWO TOTP seeds in one ceremony — the member key (today's subnet seed, unchanged) and a second admin key — and provisions both into the creator's authenticator at that single moment (ADR-0051 decision 1, CONTEXT.md 'member key / admin key'). An admin key IS a membership key; a member key is NOT an admin credential. The same ceremony captures the subnet's universal control-surface mode: prompted with NO PRESELECTION (an operator must state open or closed rather than accept a default that a hurried Enter would pick for them), with `--open`/`--closed` bypassing the prompt for scripted creation. This is the moment the whole two-key model depends on: the admin seed has no reveal verb ever (REQ-SUBNET-ADMIN-SEED-REPLICATION), so a creation path that mints it without displaying it, or displays it without persisting it, permanently destroys the subnet's admin authority with no recovery but re-minting the subnet. Gate: doc — the CONTEXT.md two-key entry and ADR-0051; impl — dual mint at create, both authenticator provisionings, the no-preselection mode prompt and its flag bypass; unit — creation yields two DISTINCT seeds, the mode is recorded from prompt or flag, and no code path yields a subnet holding one seed.
2026-09-06T13:09:02.1382698Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1382726Z 
2026-09-06T13:09:02.1382841Z ### REQ-SUBNET-ADMIN-CODE-JOIN
2026-09-06T13:09:02.1385670Z - Title: The join ceremony accepts EITHER subnet code: the SPAKE2 responder computes the member-key TOTP and the admin-key TOTP and admits a joiner proving either (ADR-0051 decision 1 — 'an admin key IS a membership key'). Indistinguishable from the joiner's side: the ceremony must not tell a joiner, or an observer of the exchange, WHICH key authenticated beyond what the protocol inherently requires — an oracle that separates admin codes from member codes turns every join attempt into a probe for which secret a machine holds. Rate limiting, transcript binding and replay defenses apply identically to both, so the second acceptable secret does not become the cheaper one to attack. Gate: doc — ADR-0051 decision 1 plus the CONTEXT.md two-key entry; impl — the responder's dual-code computation and admit path; unit — a member code joins, an admin code joins, a wrong code is refused identically to today, and the refusal/accept surface carries no key-kind discriminator.
2026-09-06T13:09:02.1385839Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1385867Z 
2026-09-06T13:09:02.1385979Z ### REQ-SUBNET-ADMIN-SEED-REPLICATION
2026-09-06T13:09:02.1389480Z - Title: Every member node holds BOTH subnet seeds — the admin seed replicates at join exactly as the member seed does — because empower verification and admin-code joins are LOCAL operations any member node must be able to serve without consulting a privileged peer (ADR-0051 decision 2; the mesh stays symmetric, authority rides a key rather than a node). Carries the NO-REVEAL invariant, which is the load-bearing half: there is no admin show-code, no QR re-provision, no `--json` field, no debug/diagnostic/error/log rendering of the admin seed ANYWHERE — it is displayed exactly once, at creation, and thereafter no node can leak what no node can reveal. A lost authenticator entry is irrecoverable by design (ADR-0051 decision 3): re-mint the subnet. The invariant binds tests too — a test that asserts the seed's VALUE by printing it is itself the leak (secret-probe discipline: never expand a secret into an observable sink). Gate: doc — ADR-0051 decisions 2 and 3; impl — admin-seed replication on the join path and the absence of every reveal surface; unit — a joined node holds both seeds and can verify an admin code locally, and no CLI/JSON/diagnostic output path emits the admin seed.
2026-09-06T13:09:02.1389734Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1389767Z 
2026-09-06T13:09:02.1389862Z ### REQ-ACL-SUBNET-MODE-CAPTURE
2026-09-06T13:09:02.1393382Z - Title: A subnet's control-surface mode is recorded on the subnet and CAPTURED AT JOIN TIME into the joining node's own store — the W1 captured-subnet-mode table stops being a stub and lights up tier 8, the chain's bottom fallback (CONTEXT.md 'control-surface modes' resolution order). Capture, not lookup, is the point: a node's effective posture must be a local, immutable fact it consented to when it joined, so a subnet owner cannot retroactively tighten or loosen what an already-joined member enforces (that is what makes the advisory-gossip posture in REQ-ACL-MODE-ADVISORY-GOSSIP coherent rather than a race). Migration posture is explicit and must be tested: an EXISTING subnet has no captured mode, so tier 8 stays absent, the chain bottoms out open, and fleet behavior is preserved exactly — an upgrade must not silently close a running mesh. Gate: doc — the CONTEXT.md control-surface-modes entry (three levels and the resolution chain); impl — the subnet-mode record, the join-time capture into the endpoint access store, and tier 8 reading it; unit — a joined-with-closed node refuses where an unlisted subject falls through to tier 8, a joined-with-open node admits, and a pre-existing subnet with no capture bottoms open.
2026-09-06T13:09:02.1393503Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1393531Z 
2026-09-06T13:09:02.1393630Z ### REQ-ACL-MODE-ADVISORY-GOSSIP
2026-09-06T13:09:02.1396312Z - Title: A subnet-mode change gossips ADVISORILY — it produces a notification and nothing else. An existing member's EFFECTIVE posture never changes remotely: the captured mode (REQ-ACL-SUBNET-MODE-CAPTURE) is immutable except through the node's own refresh (REQ-ACL-ACCESS-REFRESH-VERB), so no remote party can reach into a member node and re-posture its gate. This is the difference between a subnet owner ADVISING members of a policy change and COMMANDING their enforcement — on a shared subnet whose members are different humans, only the former is defensible, and a producer that quietly applied would be a remote write to security material. Gate: doc — the CONTEXT.md control-surface-modes advisory-gossip sentence; impl — the notif producer on subnet-mode change, with NO apply path; unit — a mode-change gossip raises the notification and leaves the receiving node's captured mode and its decisions byte-identical.
2026-09-06T13:09:02.1396427Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1396455Z 
2026-09-06T13:09:02.1396558Z ### REQ-ACL-ACCESS-REFRESH-VERB
2026-09-06T13:09:02.1399679Z - Title: `spt api access-refresh` is MINTED THIS WAVE BUT REFUSES — the verb exists, parses and is documented, and its refusal names why: the capture-refresh is engine-room-only, and engine-room enforcement (ADR-0052) does not land until W3. Minting the refusing verb now is deliberate: it fixes the contract adapters and the engine-room brief will be built against, and it makes the wave that implements enforcement a change to ONE behavior rather than a new surface plus its gate. A refusal that merely says 'unknown command' would invite an adapter to route around it. When it does light up it updates ONLY the node's captured subnet-level fallbacks — never the node's own rules, which are the operator's, not the subnet's. Gate: doc — the CONTEXT.md capture-refresh sentence naming the verb and its engine-room-only gate; impl — the verb, parsing, and a clear refusal naming the W3 dependency; unit — invoking it refuses with the engine-room diagnostic, changes no stored state, and is not reachable as an unknown-command fallthrough.
2026-09-06T13:09:02.1399931Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1399960Z 
2026-09-06T13:09:02.1400069Z ### REQ-SUBNET-ADMIN-SEED-ROTATION
2026-09-06T13:09:02.1403961Z - Title: Evicting a node rotates BOTH subnet seeds, not just the member seed. ADR-0005 #10 made removal real revocation by rotating the seed a removed node still holds; the two-key model (ADR-0051) hands every member a SECOND durable secret, and `rotate_seed` does not touch it — so as of the two-key wave an evicted node keeps the admin seed forever. That is worse than residual admin authority: an admin key IS a membership key (REQ-SUBNET-ADMIN-CODE-JOIN), so the evicted node can REJOIN the subnet on its admin TOTP, and eviction becomes toothless against exactly the nodes that were trusted enough to hold elevated credentials. ADR-0051's consequences acknowledge admin-seed rotation as unassigned milestone work; this is that work. Minimum shape: the eviction path rotates both seeds and redistributes both over the same replication machinery the join path uses, with the one-deep prior-generation grace applying to the member seed as today (an admin seed has no re-provisioning surface, so its grace question is answered by the same replication, not by a reveal). Gate: doc — ADR-0051 amended with the rotation rule; impl — the eviction/rotation path covering both seeds; unit — a rotated subnet's admin seed changes, and a node holding only the pre-rotation admin seed neither verifies an admin operation nor rejoins.
2026-09-06T13:09:02.1404091Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1404124Z 
2026-09-06T13:09:02.1404219Z ### REQ-ER-RESERVED-ENDPOINT
2026-09-06T13:09:02.1407497Z - Title: A node has exactly ONE engine-room endpoint, and 'exactly one' is STRUCTURAL rather than policed: the engine room lives at a reserved per-node endpoint id, so a second one cannot be created any more than a directory can hold two entries of the same name (ADR-0052 decision 1). It is an ordinary agent endpoint in substrate — harness-adapter-backed, spt-hosted, minded, so it can be briefed on and reason about the node's access posture — and an extraordinary one in lifecycle: its home subnet and its bound harness adapter are settable ONLY through the create/reset ceremony (REQ-ER-PURGE-RESETS), never by an ordinary endpoint edit. Creation IS that ceremony run against an empty record — one code path, so a creation that skipped a lock a reset applies cannot exist. Gate: doc — ADR-0052 decision 1 and the CONTEXT.md engine-room entry; impl — the reserved id, the engine-room record with its home subnet and bound adapter, and the single create/reset code path; unit — the reserved id resolves to at most one record, creation and reset run the same path, and an ordinary endpoint mutation cannot change the bound adapter or home subnet.
2026-09-06T13:09:02.1407613Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1407641Z 
2026-09-06T13:09:02.1407731Z ### REQ-ER-PURGE-RESETS
2026-09-06T13:09:02.1411059Z - Title: `endpoint purge` against the engine room RESETS it rather than deleting it, and requires OS elevation to do so (ADR-0052 decision 4). Reset-not-delete is what keeps 'one engine room per node' structural: a node from which the engine room can be deleted is a node that can be left with no governance surface at all, and the recovery path would then be a create verb — the very thing REQ-ER-RESERVED-ENDPOINT removes. Elevation is the right gate HERE (unlike bring-up, REQ-ER-BRINGUP-TOTP-GATE) because the ceremony sets node-scope facts — the home subnet and the bound harness adapter — and because it must work at bootstrap, when the node holds no subnet material to prove a TOTP against. The reset is the ONLY way to change either fact. Gate: doc — ADR-0052 decision 4; impl — the engine-room arm of the purge path, its elevation requirement, and the reset outcome distinct from the delete outcome; unit — the purge decision table (engine room plus elevation yields reset, engine room unelevated refuses, an ordinary endpoint is unaffected) and a reset record retaining its reserved identity.
2026-09-06T13:09:02.1411306Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1411335Z 
2026-09-06T13:09:02.1411432Z ### REQ-ER-CEREMONY-VERB
2026-09-06T13:09:02.1415940Z - Title: The engine-room ceremony is invocable: `spt endpoint engine-room <subnet> --adapter <id>` runs the one create/reset code path (`provision_at`), reporting Created against an absent record and Reset against a present one (ADR-0052 §4a, fast-follow grill 2026-07-30). Before this verb the ceremony had ZERO production call sites — a user could not set an engine room up at all. Its gates, in refusal order: BOTH arms refuse invocation by an SPT agent, detected on env markers AND on process ancestry against the broker-hosted sessions' recorded pids (defense-in-depth in front of both arms, never a substitute for the reset elevation gate; ancestry powers only this REFUSAL, never a grant — the direction that keeps the F-024 ancestry-for-auth parking intact); an unjoined subnet refuses (no seeds to verify a bring-up code against later); an unregistered adapter refuses (bring-up fails closed on it anyway — fail at bind time instead); then creation (absent record) proceeds UNELEVATED — bootstrap friction ruled to outweigh pre-emption, the window closes permanently at the first run, docs say run the ceremony early — while reset (present record) requires confirmed OS elevation, with Unknown refusing like the purge gate. Gate: doc — ADR-0052 §4a; impl — the verb, the pure ceremony decision, the two-layer agent detection, and the provision_at call; unit — the decision table pinning agent-deny-beats-elevation on both arms, bind-time refusals before the arm split, create-unelevated across every elevation, and reset refusing on NotElevated and Unknown alike.
2026-09-06T13:09:02.1416068Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1416096Z 
2026-09-06T13:09:02.1416203Z ### REQ-SUBNET-REVOKE-ADMIN-GATE
2026-09-06T13:09:02.1419269Z - Title: `spt subnet revoke` (both forms, --force-rotate-seed included) requires the subnet's CURRENT admin TOTP — a member code is insufficient, and OS elevation stays on top answering its different question (ADR-0051 §2a, FF grill 2026-07-30). Eviction is a subnet-governance act, and the proof doubles as the identity gate for the re-surface: only the proven incumbent admin is shown the replacement key. The gate runs BEFORE any write, so a cancelled, thrice-wrong, or non-interactive attempt (refused up front — the gate needs a human and the ceremony after it needs one more) leaves roster, schedule, and seeds untouched. Acceptance is the same ±1-step window every other typed-code ceremony uses. A subnet predating the two-key model has no admin authority to prove; it proceeds loudly on elevation alone. Gate: doc — ADR-0051 §2a; impl — the per-subnet gate in the revoke path, ordered before all writes; unit — the windowed code acceptance (shared with the capture proof) and the nothing-written refusal orderings exercised at the gate seam.
2026-09-06T13:09:02.1419442Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1419470Z 
2026-09-06T13:09:02.1419570Z ### REQ-SUBNET-ADMIN-RESURFACE
2026-09-06T13:09:02.1424357Z - Title: Rotation re-surfaces the NEW admin key exactly once, to the human who just proved the OLD one, via a PARKED pending seed (ADR-0051 §2a + the parked-pending-key ruling, FF grill 2026-07-30). Choreography: after the revoke's admin gate passes, the pending admin seed is minted where the human is (never at the unattended rotation site), displayed with the same code-free capture-proof ceremony as create, proven captured, and PARKED on the rotation schedule BEFORE the tombstone write — so 'schedule exists but parked seed absent' is a corruption class, never a race. The window-close rotation ADOPTS the parked seed instead of minting; a later revoke in the same window re-displays the SAME parked key behind its own proofs (one pending key per window, however many humans capture it, one epoch bump at the close); --force is the same choreography with the window degenerate (park, rotate now, clear). DEGRADE, ruled: a close with the parked seed absent or corrupt rotates the MEMBER seed anyway — the evicted node must not stay inside member fabric waiting on admin choreography — keeps the OLD admin seed in force, and is LOUD (stderr + a node-scope notif naming the state and the recovery verb, revoke --force-rotate-seed); never a silent skip, never a bricked close. The parked seed is cleared on adoption. The member key never re-surfaces at revoke (show-code covers it). Gate: doc — ADR-0051 §2a; impl — the park/parked schedule fields, the adopt-not-mint rotation, the ceremony driver, and the degrade loudness; unit — park requires an existing entry, never overwrites within a window, round-trips, reads corrupt-as-absent; rotation adopts the parked seed and degrades member-only keeping the old admin key.
2026-09-06T13:09:02.1424619Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1424643Z 
2026-09-06T13:09:02.1424758Z ### REQ-SUBNET-CREATE-CAPTURE-PROOF
2026-09-06T13:09:02.1428424Z - Title: `subnet create` proves the admin key was captured before anything persists (ADR-0051 §2b, FF grill 2026-07-30). Ceremony order, binding: the admin material displays FIRST — labelled otpauth URI + terminal QR, with NO current TOTP code anywhere on screen, because that absence is the proof's soundness (the typed-back code cannot be produced from what the screen shows, only from an authenticator that captured the seed); the human types a current admin code back, accepted within the same ±1-step window the pairing ceremonies use; the admin material is then cleared, scrollback included; and only then does the member material display, at the very end. Creation does not complete without the proof: the mint persists only after it, so a cancel, three wrong codes, or a non-interactive stdin (refused before any material is written into a pipe) leaves NO subnet — never a subnet whose admin key nobody holds, and decision 3's irrecoverability cost stays a choice rather than an accident of a missed scan. Gate: doc — ADR-0051 §2b; impl — the phased ceremony driver, the code-free capture display, the windowed proof check, and the proof-gated persist; unit — the capture display carries the provisioning material and none of the window's current codes, and the proof check accepts exactly the ±1 window.
2026-09-06T13:09:02.1428537Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1428574Z 
2026-09-06T13:09:02.1428670Z ### REQ-SUBNET-KEY-SCREENS-LABELLED
2026-09-06T13:09:02.1436864Z - Title: The `subnet create` ceremony prints TWO secrets minutes apart, and each display LEADS WITH A PROMINENT LABEL naming which key it is — `--- ADMIN KEY (<ceremony>) ---` and `--- MEMBER KEY ---` — then states WHAT THAT KEY IS FOR in the operator's own terms rather than only how to store it. Labelling is the correctness half: two unlabelled code-plus-QR blobs in one ceremony are two chances to scan the more powerful secret into the wrong authenticator entry, and the admin key is the one that cannot be shown again. The ADMIN display names the capability the key actually confers — reaching any `spt endpoint engine-room` linked to the subnet and directing it to change the node's security mode, its access rules for the node and its endpoints, and the grants collaborating agents need — and states that the admin key DOUBLES as a member key, which is the fact deciding whether an operator must keep both. The MEMBER display names joining new nodes from anywhere, sharing with others whose nodes must join, re-viewing the key with `spt subnet show-code`, and scanning the QR into an auth app to pair again later. STRUCK, and it must not return: 'as a second entry' on the admin screen, which described the authenticator's bookkeeping — the one thing the operator does not need told — and made the more powerful of the two keys read as a footnote to the other; and the member blob's trailing re-pair sentence, whose remedy is now stated ONCE, on the bullets that teach what the key is for, because a ceremony stating a remedy twice invites the two spellings to drift. That sentence carried TWO arms and BOTH survive on the list — `show-code` and the auth-app QR — because the issue's own bullets duplicated only the first, so striking the sentence on the strength of that duplication would have dropped operator information the screen used to carry, and a loss needs a positive justification (doyle, releases#175). BOTH halves are asserted: the struck sentence must not return AND the QR arm must not go missing, the second being the row without which the loss passes silently. The one-shot warning SURVIVES the reshape and moves to its own line beneath the header: a header carrying a sentence is a header that reads as prose, and 'shown once, here, and never again' is the whole reason the admin display exists. `show-code` gains no header — it is a single-key surface with nothing to be confused with — and the member blob still carries NO admin material, which the capture-proof requirement already binds. Gate: doc — the networking overview section naming the two labelled screens and what each key is for; impl — the two composers, with the struck phrases removed at their only sites; unit — each display leads with its label, each carries its purpose list, the two struck phrases are asserted ABSENT (a later reword that quietly restored either would otherwise pass), and the ceremony word still distinguishes the create display from the rotation one.
2026-09-06T13:09:02.1437183Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1437212Z 
2026-09-06T13:09:02.1437356Z ### REQ-ACCESS-RETIRED-VERB-REFUSAL
2026-09-06T13:09:02.1444120Z - Title: A RETIRED SUBCOMMAND OF `spt endpoint access` REFUSES BY NAME RATHER THAN FALLING THROUGH ITS OWN OPTIONAL POSITIONAL AND ANSWERING PLAUSIBLY. `access` takes an optional endpoint id positionally, so once `list` and `rules` stopped being subcommands (retired when the roster views replaced them) clap handed those tokens to the POSITIONAL, and the view reported `no access entities ruled for 'list'` at exit 0 -- a well-formed, confident report about an entity nobody has ever created. Found by flynn in the v0.49.0 field verify (releases#67). THE DEFECT IS NOT A MISSING ERROR MESSAGE, IT IS AN ANSWER: the sentence is TRUE of any name nobody has ruled, so it is indistinguishable from the real result for a real endpoint, and an operator or agent still carrying the old form reads it as a report and stops looking. The same failure shape as a stale carried-forward instruction -- the surface looks diligent and is wrong. MEASURED BEFORE THE FIX, and the measurement widened the filing: `rules` falls through IDENTICALLY, so the population is the retired-token FAMILY and not the one token the issue named. The node tier is NOT affected and that was checked rather than assumed: `spt node access` declares no positional at all, so clap refuses an extra argument natively. THE REFUSAL FAILS OPEN ONTO A REAL ENDPOINT. This guard is added to a path that previously accepted EVERY positional, so it is a narrowing, and a narrowing has an unruled arm: an endpoint genuinely named `list` must stay viewable. The known-target check therefore runs FIRST and wins -- a fix that made a real target unreachable would be a new defect wearing the old one's repair. Known means the access store rules it or a local perch carries the id; the store is already loaded at that point, so the check costs no read. The token match is case-folded, because an operator who typed the old verb in any casing typed the old verb. Gate: impl -- the retired-token table with its replacement guidance, the case-folded lookup, the refusal composer naming token then retirement then the working form, and the guard placed AFTER the known-target check inside the view; unit -- a decision table whose expectations are WRITTEN OUT AS LITERALS: each retired token refuses with a nonzero exit and names its replacement, an ordinary unruled endpoint id still gets the honest roster sentence (the silence the fix must not swallow), a real endpoint that happens to BE named `list` is viewed rather than refused, the casing variants refuse alike, and every token in the table is a token the current grammar does NOT accept as a subcommand -- walked against the real parser, so a token that comes BACK as a verb cannot sit here refusing itself.
2026-09-06T13:09:02.1444487Z - Required stages: impl, unit
2026-09-06T13:09:02.1444524Z 
2026-09-06T13:09:02.1444890Z ### REQ-ACL-VIEW-ROSTER
2026-09-06T13:09:02.1454893Z - Title: Access views are roster-first (CONTEXT.md 'access entity', FF grill 2026-07-30): `spt endpoint access [<endpoint>]` lists each ruled target's access entities — the subnets, nodes, and endpoints its own rules name — grouped by type in that order, each with its rule count; a subnet or the home node also carries its mode summary; a captured-mode subnet is listed even with zero explicit rules (a mode source is part of the posture picture); external entities with no rules for the target are omitted entirely; a ruled endpoint resolves to `<id>@<node> (<shared subnets>)` where this node's registry can see it and renders bare where it cannot (ruled-but-unresolvable must not hide the rule count). The view is node-sovereign — no remote rule-read exists; viewing another node's rules means running the CLI there. Supersedes the `access list` prose view; the `--json` surface re-homes the old shape's explicit `locked` flag onto the roster envelope. Known limit, documented: an endpoint literally named after a write verb (allow/revoke/open) parses as the subcommand and loses the positional path. Gate: doc — the CONTEXT.md access-entity entry; impl — the roster builder, renderer, endpoint directory, and the superseding CLI surface; unit — the roster grammar (grouping order, counts, mode annotations, bare-vs-resolved endpoints, explicit empty line) and the parse surface.
2026-09-06T13:09:02.1455060Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1455089Z 
2026-09-06T13:09:02.1455203Z ### REQ-ACL-VIEW-DRILLDOWN
2026-09-06T13:09:02.1458317Z - Title: The granular rule list is viewable only per named ruled entity (CONTEXT.md 'access entity'): `spt endpoint access [--endpoint-rules <id> | --node-rules <node> | --subnet-rules <subnet>]` — mutually exclusive flags — renders exactly the rules whose subject names that entity, through the SAME ruleset-table renderer the engine-room briefing carries (REQ-ER-RULESET-TABLE), filtered on the TYPED rule subject rather than by re-parsing the rendered subject word (a rendered-string match is a second literal waiting to drift). A target positional scopes the slice to that endpoint's rules plus the node-scope rows that also decide for it; the drill's `--json` emits the rows verbatim. The one-flat-dump-of-everything view is deliberately gone — reading a posture is a comparison per entity, not a scroll. Gate: doc — the CONTEXT.md access-entity entry; impl — the typed filter seam and the flag surface; unit — the filter matches only its own entity kind (a subnet name equal to an endpoint id must not cross-match) and the flags parse mutually exclusive.
2026-09-06T13:09:02.1458537Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1458572Z 
2026-09-06T13:09:02.1458670Z ### REQ-ACL-NODE-VIEW
2026-09-06T13:09:02.1461048Z - Title: `spt daemon access` is the node-tier roster: the entities the node-scope rules name (node and subnet-wildcard subjects — a node-scope sender-endpoint rule cannot exist by schema), this machine's own mode with its per-surface exceptions, and the captured subnet modes — the tier every hosted endpoint falls through to, in the same item grammar as the per-endpoint view so the two scopes read as one system. There is NO `spt subnet access`: a subnet is a subject tier and a mode source, never a rule-holding target — its mode facts surface on `spt subnet status` (REQ-SUBNET-STATUS-MODES). Gate: doc — the CONTEXT.md access-entity entry; impl — the node roster builder and the daemon verb; unit — the node roster's entities, mode-exception summary, and explicit empty rendering.
2026-09-06T13:09:02.1461271Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1461300Z 
2026-09-06T13:09:02.1461409Z ### REQ-SUBNET-STATUS-MODES
2026-09-06T13:09:02.1464317Z - Title: `spt subnet status <name>` states the subnet's three mode facts (surface ruled 2026-07-30 — the per-subnet view is `subnet status`, no near-synonym `show` verb minted): the mode the subnet DECLARES as this node knows it, the mode this node CAPTURED (the enforced fallback — join-time immutable, changed only through the engine room's access-refresh), and any declared change SEEN but not adopted (`declared_seen`), named PENDING with when it was seen and the explicit statement that this node's posture is unchanged until the engine room adopts it. Absences are stated in words — a pre-mode subnet and an uncaptured fallback each say so — never rendered as blank, and the `--json` rows carry the same facts as optional fields. Gate: doc — the CONTEXT.md access-entity entry's mode-facts sentence; impl — the mode-facts resolver over the two stores that each own one half of the truth and the status-view wiring; unit — the three-way wording including both absence lines and the no-pending case.
2026-09-06T13:09:02.1464441Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1464470Z 
2026-09-06T13:09:02.1464570Z ### REQ-SUBNET-MINT-CAPTURES-MODE
2026-09-06T13:09:02.1472069Z - Title: THE MINTING NODE CAPTURES ITS OWN DECLARED SUBNET MODE, so a `--closed` subnet stops being asymmetric BY CONSTRUCTION. RCA by hertz 2026-08-01 (releases#100, two-arm live evidence, both nodes 0.49.0): `capture_subnet_mode` had exactly TWO production writers -- the JOIN arm (pairhost) and the engine-room access-refresh -- while `spt subnet create` wrote the declared mode into `subnet.json` and never touched the access store. On the MINTER the captured-subnet-mode tier is therefore ABSENT and the resolution chain bottoms out at the implicit-open tier, per-surface-blind: every joiner enforces while the minter exposes MSG, RC_VIEW/RC_ATTACH, XFER, WAKE/SUSPEND, SHELL_LINK and DIGEST to any origin. Field evidence: the minter's access.json was literally {"version": 2}, its named status view printed 'mode captured here: none - chain bottoms out open', and the joiner withheld correctly. THIS IS THE DECLARE-SITE-IS-NOT-ENFORCE-SITE CLASS: writing the posture into the record that DECLARES it is not the same act as writing it into the store that ENFORCES it, and a ceremony that does only the first ships a subnet whose stated posture is unenforced on the very node that stated it. THE CAPTURE RIDES THE PERSIST, NEVER THE RESOLVE: the mint is in-memory until the create ceremony's admin capture-proof passes (a refused proof must leave NO subnet), so a capture written at resolve time would outlive a cancelled create and record a posture for a subnet that does not exist. SWEEP DISCIPLINE IS PART OF THE FIX: `resolve` is shared by create and show-code with the CALLER persisting, so the capture belongs at the ONE persist choke point every mint path funnels through -- a per-call-site capture is one forgotten path away from re-creating the same asymmetry, which is exactly how this defect existed. NO SILENT BOOT-TIME BACKFILL, and this refusal is REQUIRED TEXT rather than an omission: the subnet record carries no minted-here provenance, so 'declared present AND captured absent' cannot distinguish 'I minted this closed' from 'I joined before subnet modes existed', and retro-capturing the latter would silently close a running mesh -- the case the design refuses. A pre-fix minter is healed ONLY by the explicit `spt api access-refresh <subnet>` verb, and the create output NAMES that verb so the operator of an already-minted subnet is not left to discover it. Gate: doc -- the docs-site access page states that the minter captures at the mint and that older minters heal only through the explicit refresh; impl -- the capture at the persist choke point, driven by the mint's own declared mode, plus the create-output line naming the heal; unit -- the mint ceremony CAPTURES the mode it declared (the row that fails on a build persisting the declaration without capturing it, i.e. on the shipped defect), a persist that is NOT a mint captures nothing (the no-backfill arm, asserted rather than assumed), and the captured value is the DECLARED one rather than a constant (a row an always-closed or always-open capture cannot pass).
2026-09-06T13:09:02.1472413Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1472442Z 
2026-09-06T13:09:02.1472552Z ### REQ-SUBNET-STATUS-MODES-EVERY-VIEW
2026-09-06T13:09:02.1476562Z - Title: THE MODE FACTS RIDE EVERY SUBNET STATUS VIEW, NOT ONLY THE NAMED ONE (ruled 2026-08-02 bag grill, releases#100). REQ-SUBNET-STATUS-MODES put the three mode facts on `spt subnet status <name>`; the BARE `spt subnet status` rendered membership shape alone (subnet, nodes, endpoints), so the precise state releases#100 exists to end -- declared closed, captured nothing, enforcing open -- read as FINE on the view an operator actually runs, and the blindness was itself part of why the defect survived in the field. The bare view therefore carries a compact per-subnet mode plus the captured-here fact for every row. ONE COMPOSER, TWO RENDERINGS: the bare view resolves its facts through the SAME mode-facts resolver and states them from the SAME wording source as the named view, so the two cannot drift into two vocabularies for one truth -- a second literal spelling of 'captured here' is the defect this clause forbids, not a style preference. Absences stay stated in words on both views (an uncaptured fallback says so; a pre-modes subnet says so) -- a blank cell reads as 'nothing to worry about', which is the exact misread this requirement removes. Gate: doc -- the docs-site access page describes ONE consistent surface rather than hedging the bare-vs-named asymmetry; impl -- the bare status view's mode facts, fed by the shared resolver and the shared wording source; unit -- the bare view states both the declared and the captured fact for every row, an uncaptured subnet renders its absence in words rather than blank, and the bare and named renderings agree on the same fixture (the row that fails the moment a second wording is introduced).
2026-09-06T13:09:02.1476662Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1476692Z 
2026-09-06T13:09:02.1476805Z ### REQ-ENGINEROOM-ADMIN-BRINGUP-EMPOWERS
2026-09-06T13:09:02.1482740Z - Title: PASSING THE ENGINE-ROOM BRING-UP GATE WITH THE ADMIN TOTP EMPOWERS THE SEAT FOR ITS HOME SUBNET AT ATTACH (ratified 2026-08-01 bag grill, releases#102; CONTEXT.md empower entry amended). NO NEW CAPABILITY -- it collapses a double-entry of ONE credential: the human who just proved the admin key at bring-up had to prove the same key again through `empower` before touching a mode, which is friction that teaches nothing and protects nothing. GATE PARITY: attach OR take is the SAME gate and yields the same grant (a take already re-proves the gate, which is why it is allowed at all). LIFETIME IS IDENTICAL TO AN EXPLICIT EMPOWER -- until session end or controller detach, detach drops it, re-attach re-proves -- so this introduces no lifetime seam of its own and nothing about empowerment revocation changes. THE GRANT IS LOUD ON BOTH SURFACES A READER ACTUALLY READS: the attach output AND the session-start briefing each state that the seat is empowered for its home subnet via admin bring-up. A silent grant is the refused shape -- an empowered seat that never says so is indistinguishable from an unempowered one until a mode change succeeds unexpectedly. TWO NEGATIVES ARE PART OF THE REQUIREMENT AND MUST BE ASSERTED RATHER THAN IMPLIED BY THE POSITIVE, because both are satisfiable by a build that passes the positive row: a MEMBER-TOTP bring-up NEVER auto-empowers (the discriminant is WHICH seed verified, not that the gate passed -- a grant keyed on the gate would empower every member bring-up), and any subnet OTHER than the home subnet still requires an explicit `empower` (a build granting the seat authority everywhere passes an admin-empowers-home assertion unchanged). TERMINOLOGY, ruled with the feature: the engine-room's subnet is its HOME subnet -- 'anchor subnet' is retired and must not appear in code, docs or help in that sense. Gate: doc -- the docs-site engine-room page states the auto-grant, its scope and its lifetime; impl -- the credential-discriminating grant at the bring-up seam (attach and take) plus both loud surfaces; unit -- an admin bring-up empowers the home subnet, a member bring-up empowers nothing, no bring-up empowers a non-home subnet, both loud statements are present in the surfaces they ride, and the grant is recorded through the same seam an explicit empower uses so its lifetime is the same by construction rather than by a parallel implementation.
2026-09-06T13:09:02.1483030Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1483065Z 
2026-09-06T13:09:02.1483169Z ### REQ-ER-BRINGUP-PROMPT
2026-09-06T13:09:02.1486450Z - Title: Bare `spt rc engine-room` prompts interactively for the bring-up TOTP instead of attaching straight into the gate's refusal (ADR-0052 §2 entry UX, fast-follow grill 2026-07-30). The prompt is the preferred entry because argv is readable by other processes on the box while a code is still valid; `--code <CODE>` stays for scripted/non-interactive use with that caveat documented in its help. Prompt rules: Esc-cancel (the ceremony-overlay pattern) and ctrl-c both cancel with NO attach attempt even when the buffer holds digits; Enter submits a non-empty buffer and is inert on an empty one; only ASCII digits accumulate, length-bounded, so pasted garbage never rides into the gate and spends ledger budget; every other endpoint never sees a prompt; and a non-terminal stdin refuses with guidance naming --code rather than blocking on a pipe. Gate: doc — the ADR-0052 §2 entry-UX note; impl — the prompt behind the bare invocation, the pure key-fold kernel, and the non-terminal refusal; unit — the key-fold rules (digits-only accumulation, bound, backspace, inert-empty-Enter, submit, and both cancel paths beating a full buffer).
2026-09-06T13:09:02.1486569Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1486607Z 
2026-09-06T13:09:02.1486703Z ### REQ-ER-BRINGUP-TOTP-GATE
2026-09-06T13:09:02.1490443Z - Title: Bringing the engine room online — and attaching a controller to it — requires a same-node CLI call PLUS a member-or-admin TOTP for its home subnet, and never OS elevation (ADR-0052 decision 2). The gate proves 'a human holding this subnet's material is at the controls', which is the question that matters for a surface that sets access posture; elevation proves only 'a process on this machine ran elevated', which every agent-spawned installer path can arrange and which says nothing about subnet authority. Either seed passes because an admin key IS a membership key (ADR-0051), and the two-acceptable-secrets budget is answered by REQ-ER-BRINGUP-ATTEMPT-BOUND rather than by refusing the admin key. Bring-up FAILS CLOSED when the bound harness adapter is missing — an engine room that cannot host its own mind must not come online half-formed (ADR-0053 spirit). Gate: doc — ADR-0052 decision 2 and the CONTEXT.md engine-room bring-up sentence; impl — the same-node CLI bring-up path, local verification of the member and admin TOTP against the replicated seeds, and the missing-adapter refusal; unit — a member code brings it up, an admin code brings it up, a wrong code refuses, elevation alone never substitutes, and a missing bound adapter refuses.
2026-09-06T13:09:02.1490614Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1490642Z 
2026-09-06T13:09:02.1490743Z ### REQ-ER-BRINGUP-ATTEMPT-BOUND
2026-09-06T13:09:02.1496650Z - Title: The engine-room bring-up gate is bounded by ONE shared failure ledger with exponential backoff, not by a per-secret count (doyle ruling 2026-07-29 on ADR-0052 decision 2). Accepting either the member or the admin TOTP would otherwise hand an attacker a doubled online-guess budget (the ADR-0051 section 1a shape); a SINGLE persisted counter covering BOTH secrets removes that arithmetic entirely — the budget is the ledger's, not the keyring's. Backoff starts at one second and doubles to a one-hour cap. TWO classes of attempt are refused WITHOUT counting, named together here so the pair cannot drift apart: attempts arriving DURING a backoff, and attempts presenting NO code at all (whose own properties are owned by REQ-ER-BRINGUP-ABSENT-CODE-UNCOUNTED, minted 2026-08-19 for releases#197). Both classes exist for one reason: in this threat model the adversary is an agent on the same box, and any countable attempt it can produce WITHOUT holding subnet material lets it ratchet the human into permanent lockout — denial of governance is the attack, not merely a nuisance. The ledger resets on success and PERSISTS ACROSS DAEMON RESTARTS: a bounce that cleared it would restore the full budget on demand (the per-process-mint defect class the access-model spec found). Failures past the third raise a notification, so brute force is loud and silence buys the attacker nothing. This local bound is explicitly NOT ADR-0051 section 1a's network rate-limit argument, which does not apply on a local path with no round trip. Gate: doc — the ADR-0052 amendment naming ledger-plus-backoff as the local bound and distinguishing it from the network argument; impl — the persisted shared ledger, the backoff schedule, the refuse-without-counting rule in BOTH its classes, and the loud notification; unit — the schedule (one second doubling to the one-hour cap), one ledger for both secrets, an in-backoff attempt refused and NOT counted, a codeless attempt refused and NOT counted, reset on success, survival across a restart, and the notification threshold.
2026-09-06T13:09:02.1496912Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1496949Z 
2026-09-06T13:09:02.1497058Z ### REQ-ER-BRINGUP-ABSENT-CODE-UNCOUNTED
2026-09-06T13:09:02.1504480Z - Title: An engine-room attach that presents NO bring-up code is refused WITHOUT spending a unit of the human's attempt budget (releases#197; ADR-0052 section 2a, second uncounted class). The ledger bounds GUESSES at a human-held TOTP, and an absent code is not a guess: it carries no candidate, can never admit, and is the truthful answer from every attach path on this box that has no human at a keyboard to read a code off. Counting it handed any local process the exact ratchet REQ-ER-BRINGUP-ATTEMPT-BOUND already forbids for in-backoff attempts — N codeless attaches shut the gate a human needs to take the controls back — which is why this MOVES that requirement's text rather than sharpening it, and why the two name the pair together. It is the same fact-about-this-node's-own-configuration shape that already makes a missing bound adapter uncounted (REQ-ER-BRINGUP-TOTP-GATE), applied one input later, and it costs the brute-force bound NOTHING: the bound is over PRESENTED candidates, every one of which still counts exactly as before. The distinction is presented-versus-absent and NEVER which-key-matched, so ADR-0051's indistinguishability is untouched — an absent code carries zero member-versus-admin bits and refusal uniformity binds codes that are neither key. The ORDER is load-bearing and unchanged: the backoff is evaluated FIRST, so a codeless attempt arriving at a shut gate is answered Throttled exactly as a wrong code is and gate state stays unreadable. The ledger is NOT WRITTEN for this outcome, extending the missing-adapter write-skip rather than writing it back unchanged, because a write that failed would turn a benign codeless attach into a ledger-unwritable refusal — a worse lie than the one being fixed. Gate: doc — the ADR-0052 section 2a bullet naming the absent-code class, its ADR-0051 reading, and the throttle-first order; impl — the presentation-typed gate input (absent versus code-with-one-verified-bit), the uncounted outcome that folds into the ledger as a no-op, its own refusal sentence and label, and the extended ledger write-skip; unit — an absent code is uncounted with the ledger struct byte-unchanged, a wrong code still counts and still shuts the gate on the same schedule, a codeless attempt DURING a backoff is still Throttled (the throttle-first order pinned by test rather than by reading order), and an absent code at a shut gate answers identically to a wrong code at a shut gate.
2026-09-06T13:09:02.1504844Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1504872Z 
2026-09-06T13:09:02.1504984Z ### REQ-ER-CONTROLLER-BOUND-POSTURE
2026-09-06T13:09:02.1508791Z - Title: The engine room is empowered ONLY while a controller is attached, and losing that controller drops its POSTURE — not its process (ADR-0052 decision 3, as amended 2026-07-29 on todlando's finding). Read literally, 'detach drops it offline' would kill a harness on detach and re-break the attach-lifecycle invariant (KNOWN-HAZARDS: an rc detach ends only the viewport, the harness keeps running and stays re-attachable — an invariant the broker's PTY ownership satisfies by construction). The buildable and equally strong reading is a posture: on detach the engine room refuses all inbound, drops every empowerment (REQ-SUBNET-EMPOWER-VERB), and stops being advertised, while the process lives and can be re-attached through the same TOTP gate. Empowerment is therefore session state with TWO named drop edges — detach and local take — each of which needs its own test; 'revoked by a restart' is not an available argument, because no restart happens (REQ-ER-RC-INTENT-LOCKS). Gate: doc — the ADR-0052 section 3a amendment; impl — the posture transition on detach and on take, covering inbound refusal, empowerment drop and de-advertisement, with no process teardown; unit — both drop edges revoke empowerments, a detached engine room refuses inbound, and neither edge terminates the hosted session.
2026-09-06T13:09:02.1508909Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1508938Z 
2026-09-06T13:09:02.1509110Z ### REQ-ER-RC-INTENT-LOCKS
2026-09-06T13:09:02.1512873Z - Title: The engine room's attach surface is locked by INTENT: rc --view is denied even same-node, remote attach of any intent is denied, and local --take is allowed (ADR-0052 decision 3). Viewing is denied because a read-only spectator on the surface that renders access rulesets is an information leak with no operator benefit — and because the same-node loopback allow precedes the resolution chain, this must be an explicit engine-room carve-out rather than a rule row that the loopback shortcut would skip past. Local take is allowed on two grounds, neither of them the original 'it forces a harness restart' (it does not — a take is a broker lease displacement and restarts nothing): the displacing controller must pass the same bring-up gate the incumbent passed, so a take is a gate attempt and is bounded by the same ledger (REQ-ER-BRINGUP-ATTEMPT-BOUND); and the displacement is LOUD, so an incumbent human cannot be silently unseated. Gate: doc — ADR-0052 decision 3 and the section 3a amendment; impl — the intent-keyed engine-room carve-out ahead of the chain, the remote-attach refusal, and the take path's gate pass plus empowerment revocation; unit — same-node view denied, remote attach denied for every intent, local take admitted only after a gate pass, and a take revoking empowerments.
2026-09-06T13:09:02.1512989Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1513075Z 
2026-09-06T13:09:02.1513176Z ### REQ-ER-INBOUND-LOCK
2026-09-06T13:09:02.1516460Z - Title: The engine room refuses ALL inbound except replies to its own outbound, with a dormant knock exemption landed in the same seam (ADR-0052 decision 3; doyle ruling (f) 2026-07-29). The lock is what keeps a minded governance surface from being reachable — and therefore promptable — by the very agents whose access it governs; the reply exemption is the stateful-firewall correlation that already precedes the resolution chain, which the engine room itself depends on to hold a conversation it started. The knock hook lands NOW rather than in the knocking wave because accepting knocks is part of the lock's shape as specced, and a security-critical seam reworked twice is a seam whose second version is reviewed against the first instead of against the requirement; W3's hook default-refuses, W4 fills it with knock semantics. Gate: doc — ADR-0052 decision 3 and the CONTEXT.md engine-room entry; impl — the inbound lock riding the reply-exemption seam plus the dormant knock hook; unit — a reply to its own outbound passes, an unsolicited inbound of every other shape is refused, and the dormant hook refuses today without a knock surface.
2026-09-06T13:09:02.1516704Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1516732Z 
2026-09-06T13:09:02.1516832Z ### REQ-ER-NOT-ADVERTISED
2026-09-06T13:09:02.1520201Z - Title: The engine room is NOT registry-advertised by default, and is advertised only to endpoints it has whitelisted (ADR-0052 decision 3). An advertised governance surface is a discoverable one, and discoverability is the first half of every reach attempt the inbound lock then has to refuse; keeping it out of the feed means the agents on a node cannot even name the thing that governs them unless it has chosen to be nameable to them. This rides W1's DISCOVER gate rather than minting a parallel visibility notion — one advertisement filter, one place to reason about who sees what. De-advertisement is part of the posture drop (REQ-ER-CONTROLLER-BOUND-POSTURE): an engine room without an attached controller is not merely unreachable but unlisted. Gate: doc — ADR-0052 decision 3; impl — the advertisement filter excluding the engine room by default and honoring its whitelist, reached through the existing DISCOVER gate; unit — a default engine room is absent from the local advertisement, a whitelisted viewer sees it, a non-whitelisted viewer does not, and a posture-dropped engine room is absent regardless of whitelist.
2026-09-06T13:09:02.1520319Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1520346Z 
2026-09-06T13:09:02.1520443Z ### REQ-SUBNET-EMPOWER-VERB
2026-09-06T13:09:02.1524494Z - Title: empower <subnet-id> --admin-code <admin-totp> grants the engine room authority over that subnet's control-surface modes, lasts until session end or controller detach, and is invocable ONLY by the engine room (ADR-0052 decision 5; doyle ruling (b) 2026-07-29). It ships as an spt api verb gated by the perch capability proof plus an engine-room identity check, NOT as a plain CLI verb: the caller IS an agent — the engine room has a mind and is the specified invoker — so the gate cannot be 'no agent' and must be 'no agent except the reserved one'. The admin TOTP remains the authority proof, verified locally against the replicated admin seed (REQ-SUBNET-ADMIN-SEED-REPLICATION) with no privileged peer consulted; the engine-room binding is what makes the grant session state with a controller lifecycle to die with, which a grant to an arbitrary CLI session would not have. One shared authentication function serves this verb and the capture-refresh (REQ-ACL-ACCESS-REFRESH-ER-ONLY) — two call sites, one gate, unit tested once. Gate: doc — ADR-0052 decision 5 and the CONTEXT.md empower entry; impl — the api verb, the shared engine-room authentication, local admin-TOTP verification, and session-scoped grant storage; unit — a non-engine-room caller is refused, a wrong admin code is refused, a valid grant authorizes mode writes for exactly that subnet, and the grant dies at detach and at take.
2026-09-06T13:09:02.1524685Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1524714Z 
2026-09-06T13:09:02.1524808Z ### REQ-ACL-NODE-MODE-SET
2026-09-06T13:09:02.1527903Z - Title: The node's control-surface modes are settable through the engine room and nowhere else (ADR-0052 decisions 1 and 3, CONTEXT.md 'control-surface modes' — the node level of the three). Modes are exactly what a confused or adversarial agent would loosen, so the mutation surface must be the one place an agent cannot reach without passing a human-held TOTP; every other candidate — a plain CLI verb, an elevation-gated verb, a config file the daemon reads — is reachable by something running as the user. Subnet-scope mode authority is separate and rides empower (REQ-SUBNET-EMPOWER-VERB); this requirement is the node's own posture, which needs no empowerment because the bring-up gate already proved subnet membership. Gate: doc — the CONTEXT.md control-surface-modes entry naming the engine room as the node-level setter; impl — node-scope mode writes reachable only through an engine-room-authenticated path; unit — an engine-room caller sets a node mode, every other caller is refused, and the resolution chain reads the written mode at its node tier.
2026-09-06T13:09:02.1528150Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1528179Z 
2026-09-06T13:09:02.1528284Z ### REQ-ACL-ACCESS-REFRESH-ER-ONLY
2026-09-06T13:09:02.1531872Z - Title: spt api access-refresh stops refusing and becomes real, gated to the engine room, updating ONLY the node's captured subnet-level fallbacks (ADR-0052 decision 6; the W2 refusal REQ-ACL-ACCESS-REFRESH-VERB was minted precisely so this wave changes one behavior rather than adding a surface and its gate). The refresh is the CONSENTED half of advisory gossip: a subnet-mode change reaches a member as a notification, the engine room is briefed with the exact new posture, and a human decides whether this node adopts it — which is why the verb writes the captured fallbacks and never the node's own rules, and why no remote actor can invoke it. Its authentication is the same shared engine-room function empower uses. The W2 refusal test is repinned to the new contract in this same change, not left asserting a behavior the wave removed. Gate: doc — ADR-0052 decision 6 and the CONTEXT.md capture-refresh sentence; impl — the real refresh behind the shared engine-room gate, writing captured subnet fallbacks only; unit — an engine-room caller refreshes and the chain's bottom tier changes, a non-engine-room caller is refused, the node's own rules are untouched, and the repinned W2 test asserts the new contract.
2026-09-06T13:09:02.1531995Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1532025Z 
2026-09-06T13:09:02.1532120Z ### REQ-ER-SESSION-BRIEFING
2026-09-06T13:09:02.1542570Z - Title: Every engine-room session opens with a system-authored briefing message stating the node's exact current access posture, any pending advisory-gossip deltas, the current ruleset, the session's empowerment with its spendable verbs (empower, access-refresh), and the seat-authority statements (one-seat lifetime, no cross-node reach, no unchosen advertisement) (ADR-0052 decision 3 and decision 6; doyle ruling (e) 2026-07-29). AMENDED BY REPLACEMENT in the KEYSTONE #182 W3 lane (doyle ruling on releases#179, 2026-08-19): capabilities and responsibilities LEFT this requirement and became the engine room's durable in-core role (REQ-ER-ROLE-STATIC-IMMUTABLE), because they are static facts about the seat rather than per-session weather. The replacement is recorded here rather than annotated because a partition ruling falsifies the requirement's own ratified sentence, not just the code beneath it — a refactor UNDER a requirement is a different act from one that makes its sentence false. What stays is the per-session half: a minded governance surface is only as good as what it knows at the moment it acts, and posture is per-session state — which is why the briefing is a message spooled at bring-up under a reserved system author, NOT the endpoint's durable role text. THE BRIEFING WRITES NO ROLE TEXT: that pin is unchanged and is more load-bearing after the partition, not less, since the engine room's role now has no writer at all. The message spool is already core-side, harness-independent data that any adapter renders as ordinary inbound, so the briefing needs no harness-specific cooperation — the boundary rule holds by construction. EXTENDED for releases#177 (doyle dispatch 2026-08-19): the cadence this sentence always named is now enforced at the ENQUEUE seam — a session opens with a briefing ONCE, not once per seat-taking attach. The endpoint keeps running between attachments, so the pre-#177 product re-briefed a human who detached and came back to the SAME live session with a posture statement they had already read; "every session opens with a briefing" was true of the first attach and false as a cadence. A FRESH session — a new bring-up, or a daemon restart that re-hosts one — briefs exactly as before, which is why the bound lives on the broker's per-session log rather than in an on-disk last-briefed record: such a record would keep suppressing across a daemon restart whenever the harness outlived the broker and kept its session id, silencing the one case that must still brief. CONDITION IS NOT CADENCE: nothing here changes what a briefing says or when one is composed. The bound is on the ENQUEUE ALONE and never on delivery — REQ-ER-BRIEFING-PRESENTED clause 4 rules that a briefing whose presentation missed is retained and re-offered at the next seat-taking attach, so gating the presentation call on "did this seat enqueue" would fix #177 by re-breaking #164. Gate: doc — ADR-0052 decision 3's briefing clause and the CONTEXT.md engine-room entry; impl — the briefing composer reduced to per-session facts, spooling at bring-up under a reserved system author; unit — bring-up spools exactly one briefing carrying current posture, pending gossip deltas, the ruleset, the session's empowerment with its spendable verbs and the seat-authority statements, and no role text is written, AND a second seat-taking attach on the same session spools no second briefing while the first one still lands in that same run (the positive control that keeps the green from being a green nothing could red); int — a real second seat on a running engine-room session, pinned to be the same session by its session id, adds no briefing row.
2026-09-06T13:09:02.1542915Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.1542943Z 
2026-09-06T13:09:02.1543053Z ### REQ-ER-BRIEFING-SESSION-SCOPED
2026-09-06T13:09:02.1551441Z - Title: An engine-room briefing is SESSION-SCOPED: it states one session's weather, so a briefing that outlives the session it was minted for is stale by construction and must never be delivered (releases#208, doyle ruling 2026-08-21). The scope is enforced at SESSION OPEN, not at drain — the same predicate brief-once (REQ-ER-SESSION-BRIEFING, releases#177) already computes when it decides 'new session, enqueue a briefing' ALSO drops the undelivered briefing rows earlier sessions left behind, ordered immediately before this session's own spool so it cannot take the row it is about to mint. One site, one predicate; the drain stays dumb and learns nothing about sessions. SCOPE: UNDELIVERED rows of the reserved briefing author only — a DELIVERED row is history, cannot participate in a burst, and deleting it would be a retention change nobody filed; another author's pending mail is never touched. The count is reported, never a silent deletion, and a sweep that fails is LOUD but never blocking (stale rows reachable is a condition to state, not a reason to deny a proven human their controls). TWO MECHANISMS ARE RULED OUT WITH REASONS. (1) The `ephemeral` persistence axis: its deletion set IS the retention set of the releases#164 retained-row rescue — evaporation fires on the rows the drain RELEASED, i.e. the briefing whose presentation MISSED, which is the single case that rescue was built for; the native drive arm never evaporates at all, so the two arms would disagree about whether a briefing survives its own miss (measured at c62904e7: claim, deliver, release, then evaporate, idle arm only). (2) A time TTL: DEFAULT_TTL_SECONDS stays 0 — a time bound would evaporate a live session's own undelivered briefing on a slow attach AND still leak a stale one on a fast restart, wrong in both directions, because the honest scope is a SESSION and not a duration. The releases#164 intra-session rescue is UNCHANGED: the sweep fires only when the session is NEW, never between seats of one session, and presentation stays unconditional — this changes RETENTION, not presentation. Field measurement that opened it: six briefings minted across sixteen days on one node, delivered in a single burst oldest-first, three of them asserting an EMPTY ruleset in vocabulary the tree had already retired. Gate: doc — the CONTEXT.md engine-room briefing-retention sentence; impl — the sweep seam and its call at the session-open predicate; unit — the seam drops one author's undelivered rows and leaves that author's DELIVERED rows and every other author's pending rows intact, and an empty sweep is a zero rather than a failure; int — rows minted under session N then session N+1 opens: exactly ONE briefing delivers and it is the NEW one (the positive control is required — a fix that deletes everything satisfies the negative alone), and a seat that fails to present in ONE session still has its row re-offered to the next seat of that SAME session.
2026-09-06T13:09:02.1551770Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.1551799Z 
2026-09-06T13:09:02.1551910Z ### REQ-ER-BRIEFING-PRESENTED
2026-09-06T13:09:02.1563487Z - Title: The engine-room session briefing is PRESENTED, not merely spooled: it reaches the session's input before its first actionable turn (releases#164, doyle's five ruled clauses; root restated from measurement and ACCEPTED 2026-08-19). REQ-ER-SESSION-BRIEFING is satisfiable by an INSERT that nobody reads, which is the false confidence #164 names — this requirement is the consuming half, and the two are only meaningful together. THE DEFECT IS TIMING, NOT ELIGIBILITY, and that distinction was measured rather than argued: at the seat the ER perch is already a fully eligible spt-hosted inject target (controllable=Some(true), online, no relay — all three legs of is_spt_hosted_no_relay true), the row is spooled non-deferred and claimable, and the EXISTING idle-inject leg takes and delivers it correctly the moment the session declares idle (audited taken_leg=idle-inject, pending 1->0). Nothing is unreachable and no new taker is needed. What is wrong is WHEN: the row's only taker fires at the session's FIRST IDLE, which by construction is AFTER the first actionable turn, while clause 5's bar is BEFORE it. A window-gated arm cannot meet that bar. TWO EARLIER ROOTS ARE FALSIFIED and must not be rebuilt on: the `.idle` sentinel HAS a production writer (the `spt api state idle` seam), and the engine room is NOT adapter-less (its bring-up refuses outright unless its bound adapter is a registered HARNESS adapter). The remedy rides the NATIVE arm, which this codebase already defines as exactly this case — the window owns WHEN, native owns THROUGH WHAT — and which was proven to carry into a never-yet-idle ACTIVE ER session on the EXISTING surface before a line of the fix was written. Riding native satisfies every ruled clause by REUSE rather than new machinery: the same delivery path and the same single renderer (clause 2 forbids a second renderer), the same BEGIN IMMEDIATE claim kernel and taken_* stamping so a poll race cannot double-present (clause 3 — exactly-one-taker by claim, not by timing), and miss => release => row retained pending plus a loud post-seat notice, with the attach NEVER refused (clause 4, extending the denial-of-governance doctrine already at the spool-failure arm). Clause 1 is untouched: #142 post-seat composition is not relitigated. Gate: impl — the post-seat native drive of the briefing through the existing spt-hosted inject leg, reusing the audited claim and the single renderer, with the loud-notice-and-retain arm on presentation failure and no refusal path added to attach; int — a real mock ER session in which the briefing reaches session input BEFORE the first actionable turn, the spool audit shows exactly one taker, and the never-seated arm shows zero, driven end to end through the real verbs; AND the MISS half of clause 4 witnessed on the same real verbs, because a fence that only ever sees the happy path is not the fence that was ruled — a seat whose harness never binds must make the failure LOUD (and only after the drive's bounded wait is honestly spent, since an early give-up turns a merely slow bind into a lost briefing: the same defect, rarer and harder to see), must leave the row RETAINED and pending with its taker columns UNSTAMPED, and must NOT refuse the attach; and then the next seat-taking attach WITHIN THE SAME SESSION must RE-OFFER that retained row and deliver IT — asserted on the row's own id rather than on the arrival of any briefing. At a SESSION boundary the retained row is instead SUPERSEDED under REQ-ER-BRIEFING-SESSION-SCOPED: swept loudly at the new session's open, with the new session's own briefing delivered at that same attach — the chain proven cross-session is miss => release => retained => superseded-with-delivery, never a silent loss. The eligible-but-inject-FAILS shape is a DECLARED residual (doyle's ruling 2026-08-19): its only unique witness is the second loud branch, the claim-then-release it would exercise is the same shared drain path the arms above already walk, and it earns a cell if that branch ever fires unwitnessed in the field.
2026-09-06T13:09:02.1563810Z - Required stages: impl, int
2026-09-06T13:09:02.1563839Z 
2026-09-06T13:09:02.1563943Z ### REQ-ER-ROLE-STATIC-IMMUTABLE
2026-09-06T13:09:02.1571583Z - Title: The engine room's role is a STATIC, IMMUTABLE value served from core — its seat line, its rule-tier semantics, its control-surface vocabulary and its discipline — and it has NO WRITER AT ALL (releases#179, greenlit; doyle rulings 2026-08-19 in the KEYSTONE #182 W3 lane; the ratified first-output skeleton is releases#165). Every other endpoint’s role is durable identity authored by its owner and carried by tracked/agents/<id>/live-role.md; the engine room is the degenerate case, because its seat is minted by the node rather than chosen by a person, so there is nothing for an owner to author and a per-session writer would race the role editor and push session weather into the mind's durable substrate. Serving it from core is what makes IMMUTABLE a property of the system rather than a promise: BOTH readers of role text — the resume path’s <live-role> emission and the bare-read CLI arm — serve the core value for engine-room, so an on-disk live-role.md planted for that id is ignored dead weight and is never consulted, and the sole writer (the --overwrite arm) REFUSES engine-room loudly, naming the in-core role as the reason. The carrier is a FUNCTION and not a const, and that is load-bearing rather than stylistic: a const cannot call the shared surface composer, so a const role would have to hardcode the surface list — the exact defect REQ-ER-BRIEFING-SURFACE-VOCAB refuses — while a function is still a static value in the sense #179 asks for (no store read, no writer, no per-session input, deterministic output). The role is also PROGRAM TEXT: it prescribes verbs, so every verb it prescribes is walked against the clap grammar over its WHOLE span, re-derived rather than sampled. Gate: doc — the ratified ER skeleton artifact landed under docs/ and the CONTEXT.md engine-room entry stating the role is served from core and has no writer; impl — the in-core role composer building its control-surface section from the shared table composer, both readers serving it for engine-room, the --overwrite refusal, and the briefing composer reduced to per-session facts; unit — the grammar walk over the whole role text, both readers serving the core value while IGNORING a planted on-disk live-role.md for engine-room, --overwrite refusing engine-room with a non-ER-succeeds discriminator, and both skeleton-amendment controls (a panic arm for the kept-ellipsis fault and a PRESENCE arm for the silently unwalked continuation cell, because a silent hole walks clean past a panic expectation and the two faults cannot share one control); int — an engine-room bring-up serving the in-core role, declared first-execution.
2026-09-06T13:09:02.1571785Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.1571813Z 
2026-09-06T13:09:02.1571908Z ### REQ-ER-RULESET-NODE-NAMES
2026-09-06T13:09:02.1577295Z - Title: A node subject in a ruleset row renders the node's NAME (`node:ENLYZEAM`), not its pubkey hex — the second half of releases#210's unreadability (the first is the table shape, REQ-ER-RULESET-TABLE). Resolution goes through an INJECTED resolver `Fn(&str) -> Option<String>` on the ruleset-rows seam (the `derive_attached_node` precedent): the store crate holds no registry edge, and a render provable with no filesystem, no node and no gossip is worth more than one that reaches for a name itself. Consumers supply the real lookup — this node's own subnet rosters and hostname, layered under the gossiped registry labels the picker and `endpoint list` already render from, so every view on a box names a node the same way. DEGRADE (ruled, doyle 2026-08-21): an unresolvable node renders the FULL 64-hex — never an error, never blank, and never a truncation, because a truncated hash is the filed defect itself. A whitespace-only label has not answered and degrades identically. The JSON keeps the IDENTITY: `subject` stays the full hex and the resolved name rides beside it as an ADDITIVE field, omitted rather than null when unresolved — a machine keys on the pubkey and a label is not a key (the human/JSON asymmetry rule releases#206 paid for). Row ORDER follows the identity, never the name: the table is diffed across postures, and a label arriving or going away must not reorder the grid. Gate: doc — the CONTEXT.md engine-room ruleset sentence and the access-viewing drill-down section; impl — the resolver seam on the rows builder, the subject render, and the two consumers' real resolvers; unit — a resolved subject renders the name with the hex absent from the cell, an unresolvable one keeps the full untruncated hex, a blank label degrades, only node subjects resolve, the JSON keeps the hex and carries the name additively (omitted when absent), and naming does not reorder rows.
2026-09-06T13:09:02.1577562Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1577590Z 
2026-09-06T13:09:02.1577680Z ### REQ-ER-RULESET-TABLE
2026-09-06T13:09:02.1582893Z - Title: The engine room presents access rulesets as TABLES (ADR-0052 decision 3). The rendering is a requirement rather than a nicety because the operator decision this surface exists to support — is this node's posture what I think it is — is a comparison across subjects, surfaces and tiers, and prose forces a human to hold that grid in their head while an agent narrates it to them. A table also makes an omission visible: a row that should be there and is not is legible in a grid and invisible in a paragraph. It rides the same briefing message the session opens with (REQ-ER-SESSION-BRIEFING) and the same renderer serves an on-demand ruleset request. Gate: doc — ADR-0052 decision 3's table clause; impl — the ruleset table renderer used by the briefing and by an on-demand request; unit — the renderer emits one row per rule with subject, surface, tier and decision, renders an empty ruleset as an explicit empty table rather than silence, and is stable enough to diff across two postures. AMENDED releases#210 (doyle ruling 2026-08-21): the rendering is a MARKDOWN pipe table with space-padded cells — valid markdown in the chat surface an agent relays it through AND still column-aligned in a terminal, so the one renderer serves both consumers (the briefing and the drill-down view) and the grid cannot fork into two spellings. A space-aligned grid is reflowed into a run-on by the surface a human actually reads it on, which is the defect: the table stopped being a table in transit. Cell content is escaped at the row edge so a value containing a pipe cannot silently stop the grid from being a grid. Unit additionally: the header, the separator row that makes it a markdown table, one pipe-delimited row per rule with a fixed cell count, the empty case still explicit, and the pipe-bearing cell.
2026-09-06T13:09:02.1583065Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1583093Z 
2026-09-06T13:09:02.1583184Z ### REQ-CI-LOCAL-GATE-DISPOSAL
2026-09-06T13:09:02.1585598Z - Title: Local gate-battery parity (path-scoped reap before target disposal + free-byte proof): a local self-hosted gate battery that creates a throwaway Cargo target reaps only process trees owned by that battery, with ownership proved by executable path or owned temporary/cache root under its workspace or declared target roots — never by process name alone; proves the scoped survivor census is zero BEFORE deleting the target; and reports drive free bytes before and after disposal. It MUST NOT machine-wide kill spt processes. Evidence: six Phase-B child daemons outlived suite completion for about two minutes in-job and indefinitely in local rigs; foreign job processes exited with the job; post+30s scoped census was zero. This extends existing CI end-reap semantics to local gate rigs, not production lifecycle behavior.
2026-09-06T13:09:02.1585833Z - Required stages: 
2026-09-06T13:09:02.1585861Z 
2026-09-06T13:09:02.1585988Z ### REQ-HAZARD-PROCESS-GLOBAL-ONE-RESOLUTION
2026-09-06T13:09:02.1588341Z - Title: Seed: resolve a process-global exactly ONCE per public operation and thread that value inward; a public function that resolves the global and calls a callee which resolves it again contains a latent disagreement. Field-near discovery: `daemon_inhibit::set_stop_inhibit -> inhibit_path` and `servicehost::service_env -> env assembly` both carried this production shape, invisible until a test became the victim. The eventual regression must pin SINGLE RESOLUTION PER OPERATION, not merely the presence of an `_at(home)` seam: count `spt_home()` occurrences in each file and require exactly one inside each thin public boundary (current repaired shape: servicehost one occurrence in its wrapper; daemon_inhibit five, one per public function), so no inner callee can silently re-read the process-global. Invariant repair: resolve once at the public boundary, thread the value inward. Design source: KNOWN-HAZARDS 7.57.
2026-09-06T13:09:02.1588446Z - Required stages: 
2026-09-06T13:09:02.1588474Z 
2026-09-06T13:09:02.1588594Z ### REQ-HAZARD-TEST-PID-TREE-KILL-IDENTITY
2026-09-06T13:09:02.1594744Z - Title: Seed (DE-SCOPED from TEST-only, BAROMETER 2026-08-02 — the id keeps its `-TEST-` infix for registry stability, but the rule always covered both populations and the product half is now proven, tracked by REQ-SHELL-KILL-AUTHENTICATED): any process kill driven by a pid read from a best-effort breadcrumb FILE — tree or single-process, IN A TEST RIG OR IN SHIPPED PRODUCT CODE — must first distinguish an already-exited process (benign: log and skip) from a live process, then prove the live process is the one the record MEANS before terminating it: in a rig by re-resolving its executable path and requiring it under that test job's own target directory (self and test-harness ancestors always refused); in product code by the pid+birth PAIR via `liveness::relay_liveness`, since the daemon can park a birth stamp at launch and an image path cannot separate two shells sharing one binary. A live but unidentifiable process is NEVER a legitimate kill target in either population, and the refusal must be AUDIBLE — a destructive path that treats unknown as do-not-fire trades a loud wrong action for a quiet wrong state unless it names the pid it spared and why. `/T` grades blast radius; it is not the defect. Field trigger: golden run 30501468421 Windows job 90742055246 ended worker_lifecycle_e2e with bare exit 1 after every assertion and observability point had been placed after taskkill /PID <daemon.pid> /F /T. Predicate-bounded breadth on crates/*/tests/*.rs: 42 taskkill-/T files; 30 also reference a literal pid/ready breadcrumb filename; 23 breadcrumb-to-tree-kill flows manually confirmed and 7 unconfirmed. brain.ready dominates (26 references), and its heartbeat refresh makes staleness correlate with the original target already being dead — precisely when pid reuse bites. An independent wider-root predicate reproduced exactly the original 6 daemon.pid tree-kill flows. A graded second tier adds 12 manually-confirmed daemon.pid taskkill-/F files without /T: smaller blast radius, identical unauthenticated-termination defect.
2026-09-06T13:09:02.1595013Z - Required stages: 
2026-09-06T13:09:02.1595042Z 
2026-09-06T13:09:02.1595148Z ### REQ-ROSTER-SELF-LEASE-HEAL
2026-09-06T13:09:02.1601289Z - Title: WAN-TRUTH #40 F1: a node's OWN advertised roster row can never be frozen by a regressed epoch counter. Before authoring a self-entry — at pump boot and on every roster-exchange provider pass — the node consults the fleet-replicated lease it already holds for its own pubkey (RosterStore, the copy peers merged back to it) and fast-forwards its EpochSource strictly past that ceiling, so every self-advertisement out-ranks the copy the fleet holds and merge_entry's strictly-greater rule admits it. Field shape this repairs: HFENDULEAM's counter restarted at 0 after a hard reset (NUL husk, see the twin) while its fleet-replicated self row carried lease 803460 with a Jul-18 port; every later self-advertisement merged Stale on every peer, forever, and the node could not repair its own advertised address by any means short of operator surgery — the defect that makes a rebind (F4) invisible and strands the node symmetrically. The heal is retroactive: it repairs an already-damaged node on its next boot, which is why it, and not counter durability, is the primary fix; the durable epoch write and the loud corrupt-husk log ride along as defense in depth, and atomic.rs stops naming epoch counters as safe-for-non-durable RECONSTRUCTIBLE state (that sentence is now a documented-false invariant). Twin: REQ-HAZARD-SELF-ROW-LEASE-FREEZE. Kin REQ-MESH-2 (the upsert_self authorship site), REQ-HAZARD-REGISTRY-EPOCH-LEASE (the counter's own monotonicity contract, unchanged here). Gate: impl — the fleet-lease ceiling read, the fast-forward, its wiring at boot and at the provider, the durable write and the husk log; unit — a counter below the fleet ceiling fast-forwards strictly past it, a counter already above it is untouched (never a gratuitous jump), a healed node's next self-entry merges Updated (not Stale) against the stale high-lease row, and a NUL husk is distinguished from an absent file in the log; int — a node booting with a regressed counter and a high-lease stale self row re-advertises a self entry the fleet admits, with zero state surgery.
2026-09-06T13:09:02.1601481Z - Required stages: impl, unit, int
2026-09-06T13:09:02.1601509Z 
2026-09-06T13:09:02.1601613Z ### REQ-HAZARD-SELF-ROW-LEASE-FREEZE
2026-09-06T13:09:02.1604934Z - Title: HAZARD: a node whose epoch counter has regressed below the lease on its own fleet-replicated roster row is FROZEN — it can never again change its advertised address, on any peer, and nothing in the system says so. The failure is silent by construction: the node keeps authoring self-entries at a lower lease, every peer's merge_entry returns Stale, net_up stays true, the heartbeat stays fresh, and the roster rows peers hold keep a fresh last_seen riding a dead address. Regression is near-deterministic on a hard reset because the counter is rewritten every few seconds through the non-durable atomic path and resurrects as a NUL husk that parses to absent and reloads as 0 (evidenced twice: ENLYZEAM Kernel-Power 41 seventy-two seconds after its last winning row; HFENDULEAM Jul-19 resets against a Jul-18 frozen row). Forbidden state: a node advertising a self-entry the fleet cannot admit, with no self-repair path. Twin of REQ-ROSTER-SELF-LEASE-HEAL. Gate: doc — the KNOWN-HAZARDS entry; int — the frozen-row shape is set up on a real node root and the boot heal is proved to break it (a pre-fix build strands).
2026-09-06T13:09:02.1605105Z - Required stages: doc, int
2026-09-06T13:09:02.1605133Z 
2026-09-06T13:09:02.1605237Z ### REQ-DIAL-FAILED-ADDR-MEMORY
2026-09-06T13:09:02.1609210Z - Title: WAN-TRUTH #40 F2/F3 mechanism: the node remembers the exact ADDRESSES its dials just failed on — never the peer, never the chain leg (doyle ruling 2026-07-31, amending the RCA's per-leg/per-peer wording, which would have deleted the strand-heal guarantee REQ-PEER-ROUTE-CHAIN was built to provide). The record is keyed on the full address actually dialed, compared by exact equality; it is TTL-bounded and cleared by a successful dial or validation of that address, so a transient failure can never permanently ban an address that works. It is RAM-only and node-LOCAL: a restart forfeits it (one wasted dial, re-marked on the next failure), no new persisted store is introduced, and the fact never rides the gossip/roster DTO in any form. It is BUILT at the dial site (which owns the address that was dialed) and consumed by its two leaves — REQ-ROUTE-CHAIN-FAILED-ADDR-SKIP and REQ-RECONCILE-FAILED-ADDR-REFUSE — through ONE shared helper, so neither consumer re-derives the equality. Gate: impl — the type, its construction at the dial site, the clear-on-success, the TTL, and the carry to the pump-side consumer over the existing local presence field; unit — an exact address matches and a differing address does not, a successful dial clears the record, an expired record stops matching, and the type carries no serialization onto any gossip DTO.
2026-09-06T13:09:02.1609457Z - Required stages: impl, unit
2026-09-06T13:09:02.1609481Z 
2026-09-06T13:09:02.1609594Z ### REQ-ROUTE-CHAIN-FAILED-ADDR-SKIP
2026-09-06T13:09:02.1612341Z - Title: WAN-TRUTH #40 F2 (the ROUTE-CHAIN consumer of REQ-DIAL-FAILED-ADDR-MEMORY): leg 2 of resolve_submit_addr skips a validated roster address that IS the address the last dial failed on, falling through to leg 3 (id-only discovery) — the leg the field incident could never reach. Every other roster address still resolves exactly as today: this is a skip of one known-dead value, NOT a demotion of the roster leg, so a node holding a genuinely fresher roster address still heals connection-free (REQ-PEER-ROUTE-CHAIN's guarantee, re-pinned by intent rather than inverted). Sibling: REQ-RECONCILE-FAILED-ADDR-REFUSE, the same memory read at the reconcile consumer. Gate: impl — the leg-2 skip through the shared helper; unit — a roster address equal to the just-failed one falls through to discovery, a roster address DIFFERING from it still resolves at leg 2 (the strand-heal intent), and with no failure recorded the chain order is unchanged.
2026-09-06T13:09:02.1612455Z - Required stages: impl, unit
2026-09-06T13:09:02.1612483Z 
2026-09-06T13:09:02.1612579Z ### REQ-RECONCILE-FAILED-ADDR-REFUSE
2026-09-06T13:09:02.1615693Z - Title: WAN-TRUTH #40 F3 (the RECONCILE consumer of REQ-DIAL-FAILED-ADDR-MEMORY): reconcile_peeraddrs refuses to reinstall over a suspect cache row the very address whose dial just failed, and leaves the suspect mark standing — while still installing any DIFFERING validated roster address and clearing the mark exactly as today. This kills the livelock the live-fire injection proved (fail at 19:42:19 -> demote -> the 19:42:34 roster exchange reinstalls the byte-identical stale address and clears the mark -> fail), without removing the connection-independent recovery the same function exists to provide. Sibling: REQ-ROUTE-CHAIN-FAILED-ADDR-SKIP, the same memory read at the route-chain consumer. Gate: impl — the suspect-arm predicate through the shared helper, at both the startup and the roster-merge sink call sites; unit — a suspect row is NOT overwritten by the just-failed address and stays suspect, a suspect row IS replaced by a differing roster address with the mark cleared (re-pinned by intent), and absent/invariant-invalid rows behave as before.
2026-09-06T13:09:02.1615807Z - Required stages: impl, unit
2026-09-06T13:09:02.1615884Z 
2026-09-06T13:09:02.1615996Z ### REQ-HAZARD-FAILED-ADDR-REINSTALL-LOOP
2026-09-06T13:09:02.1619363Z - Title: HAZARD: a peer is never stranded by an address that has ALREADY failed — no path may re-serve or reinstall a just-failed address such that the node retries it forever and never reaches the discovery leg. This is the second clause of KNOWN-HAZARDS 7.42, added rather than carved out: 7.42's first clause (a valid roster address must never be deleted by a failed dial) remains true and is what REQ-HAZARD-MESH-BOOTSTRAP-TRAP pins; the cross-site incident falsified the unstated assumption BEHIND it, that a retained roster address is fresher than the failed one. Forbidden state: fail -> demote to suspect -> reinstall the identical address -> fail, cycling on every roster exchange while the id-only discovery leg stays unreachable and every status surface reads healthy. Kin REQ-DIAL-FAILED-ADDR-MEMORY (the mechanism that forbids it), REQ-HAZARD-MESH-BOOTSTRAP-TRAP (the first clause, distinct anchor). Gate: doc — the KNOWN-HAZARDS 7.42 second-clause entry; int — the livelock is set up on the real pump seam (dead roster address, discovery live) and the node is proved to reach discovery and connect; a pre-fix build never converges.
2026-09-06T13:09:02.1619601Z - Required stages: doc, int
2026-09-06T13:09:02.1619630Z 
2026-09-06T13:09:02.1619736Z ### REQ-SELF-ADDR-REBIND-READVERTISE
2026-09-06T13:09:02.1623148Z - Title: WAN-TRUTH #40 F4: when this node's live endpoint address changes — a rebind to a new port after a restart, the everyday shape behind the incident's dead Jul-18 port — it re-authors its own roster self-entry with the new address immediately, rather than waiting for a peer to dial in and trigger the exchange provider. A node whose address changed is exactly the node peers can no longer reach, so making propagation depend on an inbound connection is the wrong direction of dependency; the local roster row is refreshed at once (so any exchange, in either direction, serves the current address) and the advertise wake is set so the next pump round pushes it. The re-authored entry carries a strictly-greater lease, which is only sound once REQ-ROSTER-SELF-LEASE-HEAL guarantees the counter is above the fleet's ceiling — F4 is the propagation half of the same field failure, F1 the ordering half. Gate: impl — the address-change detection against the last-advertised self row, the re-author, and the advertise wake; unit — a changed address re-authors the self row with a strictly-greater lease and sets the wake, an unchanged address does neither (no advertise storm), and a null/unbound address is never advertised.
2026-09-06T13:09:02.1623274Z - Required stages: impl, unit
2026-09-06T13:09:02.1623302Z 
2026-09-06T13:09:02.1623405Z ### REQ-DIAL-ROUTE-LEG-ATTRIBUTION
2026-09-06T13:09:02.1627467Z - Title: WAN-TRUTH #41 leg 1: every submitted dial says WHAT address it dialed and WHICH leg of the route chain produced it, at submit and again on failure. The 2026-07-30 cross-site RCA had to infer the dialed address from timing alone, which is what made a days-long partial outage expensive to diagnose rather than merely bad. The attribution is a VERDICT type over the chain, not a free-text log fragment: one token per outcome, single-sourced so the log sites and the tests that match them can never spell it differently. Critically, the post-lane-1 skip is its own verdict and never collapses into plain discovery: after REQ-ROUTE-CHAIN-FAILED-ADDR-SKIP a node that HOLDS a validated roster address can still fall to discovery because that exact address is the one its last dial failed on, and reporting that as discovery asserts the opposite diagnosis -- that this node has no roster address for the peer -- which would poison the #42/#49 relay RCA this observability exists to serve. The unresolved outcome splits the same way. This requirement owns only the REPORTING; REQ-PEER-ROUTE-CHAIN continues to own the chain order and this must never change an outcome. Gate: impl -- the leg verdict type and its single-source token, returned by the resolver and logged at the submit site, at the submit-refused failure, at the no-route failure, and carried to the drained DIAL_FAILED line (unrecorded means unknown, never a guess); unit -- each chain leg resolves to its own verdict on the existing chain rig, the just-failed roster address resolves to the SKIP verdict while a differing roster address still resolves to the roster verdict, and the unresolved cases split the same way.
2026-09-06T13:09:02.1627624Z - Required stages: impl, unit, int
2026-09-06T13:09:02.1627652Z 
2026-09-06T13:09:02.1627753Z ### REQ-SELF-ENDPOINT-ADDR-SURFACE
2026-09-06T13:09:02.1631416Z - Title: WAN-TRUTH #41 leg 2: this node OWN dialable address -- id, direct paths, and the relay it is actually homed on -- is reported by spt daemon status and its --json twin. The home relay reached no surface at all before this: the fleet homed onto iroh-canary relays while daemon.json still declared N0Default, and nothing an operator could run would contradict the config file. The value is therefore read off the LIVE endpoint (the broker net-status reply, which already carried it unconsumed), never echoed from configuration -- a line that restates config would have agreed with the wrong answer. No relay advertised is a reported state, not an absence of one, and an unparseable or unknown address shape degrades to that rather than failing the status read. Distinguish from REQ-PUMP-STAGE-TRUTH, which reports whether we can reach OTHER nodes; this reports what we advertise about OURSELVES. Gate: impl -- the relay accessor over the opaque EndpointAddr JSON, the status --json fields (self_addr, home_relay, node_id_hex, all additive), and the human status line; unit -- the accessor is proved against a REAL serialized EndpointAddr rather than a hand-written fixture (this repo existing opaque-addr fixtures spell paths as bare strings, which iroh does not emit, so a fixture-verified accessor would return no relay for every node in the field), a relay-less address reports no relay, and junk degrades without panicking.
2026-09-06T13:09:02.1631579Z - Required stages: impl, unit, int
2026-09-06T13:09:02.1631608Z 
2026-09-06T13:09:02.1631716Z ### REQ-PEER-HEALTH-PARTIAL-DEGRADE
2026-09-06T13:09:02.1636048Z - Title: WAN-TRUTH #41 leg 3: a partial peer outage renders its own verdict (degraded-partial) instead of healthy. The field shape: 2 of 7 peers live and the other 5 failing for days, reported green, because the green test was at-least-one-live-connection and any single dial success also closed the only incident window there was. INSTRUMENT SOUNDNESS is the governing principle and the reason this is a requirement rather than a nicety -- the monitor green must not be producible by the fault it watches, and persistent per-peer dial failure was exactly such a producer. Two changes carry it: the failing set is consulted BEFORE the live count so green requires that nothing is failing rather than that something works, and an ANY-fail window spans the all-fail one so a single peer reconnecting narrows the verdict without erasing the incident measured age. The full-sequester verdict, its window, and the stage attribution are unchanged; this is additive on the machine surface, and an older reader meeting an unfamiliar token is the intended failure mode, since healthy is precisely what it used to be told. Distinguish from REQ-PUMP-STAGE-TRUTH (same ADR-0039 Decision 5 surface): that owns the TOTAL-failure fingerprint and the per-stage attribution, this owns only the partial-failure verdict and the window that survives one peer recovery. Gate: impl -- the verdict variant, the any-fail window recomputed with the all-fail one, the ordering change, and both render surfaces (human line and machine token); unit -- some-failing-while-others-live renders the partial verdict and never healthy, green requires progress on EVERY target, one peer success does not restart the incident clock, and the total-failure fingerprint still renders the unchanged full verdict.
2026-09-06T13:09:02.1636161Z - Required stages: impl, unit
2026-09-06T13:09:02.1636251Z 
2026-09-06T13:09:02.1636372Z ### REQ-HAZARD-MONITOR-GREEN-SELF-PRODUCIBLE
2026-09-06T13:09:02.1642452Z - Title: HAZARD: a monitor green state must never be REACHABLE BY the fault that monitor exists to report. A surface that can certify a broken system as healthy is worse than no surface, because it ends the operator investigation rather than merely failing to start it. Paid for twice on one surface in one batch: the 2026-07-10 sequester (net_up meant a local host object exists, the heartbeat meant the loop is alive, durable counts meant membership -- none was a function of reaching a peer, so all three stayed green while every dial failed), and then AGAIN in the fix for it (issue #41: the reachability verdict returned Healthy on any live peer BEFORE consulting the failing set, and its only incident window required every target to be failing, so 2 of 7 live with 5 failing for days read healthy and one dial success closed the window). The second instance is the binding lesson: at-least-one-thing-works is the intuitive spelling of green and it is precisely the spelling the fault can satisfy. Forbidden state: any state reachable by the fault that the surface reports as green. Invariant: green asserts the ABSENCE of the fault, never the PRESENCE of some success -- nothing failing rather than something working; a partial fault renders its own verdict instead of collapsing into a neighbour; an incident window closes only when the fault clears, never on one participant recovery, elapsed time, or a heartbeat. Corollary: dropping the failing subject from the monitored set produces green just as silently, so every membership-change path is a re-entry path and must be examined as one (the known open door is documented in KNOWN-HAZARDS 7.61 rather than closed here). Twin: REQ-PEER-HEALTH-PARTIAL-DEGRADE, the positive requirement repairing instance 2. Kin REQ-PUMP-STAGE-TRUTH (the total-failure fingerprint and stage attribution, produced by instance 1 and unchanged here). Distinct from the instrument-soundness rule about TEST gates, which governs a rig verdict rather than a production monitor and is never requirement evidence. Gate: doc -- the KNOWN-HAZARDS 7.61 entry naming the class, both instances, and the open eviction door; unit -- the fault at full strength (five of seven peers failing behind two live ones) is asserted NOT green in its own terms; int -- on the real pump seam, the dial path does not emit the reading it would produce in the benign case (plain discovery) while sitting in the fault (a held roster address that was refused).
2026-09-06T13:09:02.1642705Z - Required stages: doc, unit, int
2026-09-06T13:09:02.1642738Z 
2026-09-06T13:09:02.1642839Z ### REQ-PEER-COUNT-TARGET-SCOPED
2026-09-06T13:09:02.1648067Z - Title: The rendered peer-reachability fraction counts ONE population at both ends: the live count is the intersection of the pump live connections with the CURRENT fan-out targets, re-derived whenever the targets move. The field specimen was 7/1 -- seven live peer connections over one remaining membership target -- because the numerator came from the connection cache (inserted on every PRESENCE_CONNECTED, is_target computed beside it and not gating the insert, removed only on PRESENCE_DISCONNECTED) while the denominator was roster-derived. Leaving a subnet disconnects NOBODY, so ex-members went on counting while the denominator followed the leave; the state therefore travels a targets change with no presence event anywhere near it, which is why the count cannot be maintained at the presence seam alone and must be re-derivable from a live-connection SET rather than stamped as a number. The fix is an INTERSECTION and explicitly not a teardown at leave: a peer can be justified by a second subnet, so dropping its connection would make the count right for the wrong reason and wrong again the moment the peer is re-justified. Connections to non-targets stay real and stay reported; they are simply not part of the answer to how many of the peers we are trying to reach are up. Consequence, in the honest direction: the sequester test narrows, since a live connection to a node we no longer fan out to used to hold the all-fail window shut while every actual target was failing. Distinguish from REQ-PEER-HEALTH-PARTIAL-DEGRADE, which owns WHICH VERDICT a mix of live and failing peers renders; this owns only the population the counts are taken over. Gate: impl -- the live-connection set on the snapshot, the derivation against targets, the re-derivation on set_targets, and the pump seam handing the whole conns key set rather than a length; unit -- a subnet leave re-derives the numerator with nobody disconnected and the connections still reported, a re-join restores it without a redial, a live non-target never enters the count nor renders green, and the rendered line reads 1/1 where it read 7/1.
2026-09-06T13:09:02.1648296Z - Required stages: impl, unit
2026-09-06T13:09:02.1648325Z 
2026-09-06T13:09:02.1648420Z ### REQ-PEER-ABSENCE-VERDICT
2026-09-06T13:09:02.1655493Z - Title: Peer reachability has a THIRD verdict -- peers-absent, rendered `peers unreachable:` -- for the state where the failing peers are simply not there, distinct from healthy and from both degraded verdicts and never a synonym for either. The vocabulary had no way to say that machine is off: PRESENCE_DIAL_FAILED fires for any reason at all including the ordinary powered-down peer, so the word the operator read (degraded) pointed at THEIR OWN node. The discriminator is POSITIVE and INDEPENDENT, two conditions that must both hold and neither of which any peer SUCCESS can supply: every currently-failing target is a node the registry holds rows for and none of those rows is routable, AND last_registry_admit_ms is within the registry own silent-peer eviction window of the instant that offline set was sampled. Silence about a node is not evidence -- a node with no rows keeps the conservative verdict -- and stale gossip is memory rather than observation, so no fresh gossip means no basis to call anyone absent. The independence requirement is the load-bearing one: the tempting inference that a live peer proves this node egress and therefore acquits it of the other peers failures is one peer success testifying about a DIFFERENT peer failure, which is REQ-PEER-HEALTH-PARTIAL-DEGRADE (#41) outvoting logic wearing a new word; egress to X says nothing about the route to Y. The freshness gate is also what preserves the 2026-07-10 sequester fingerprint with no carve-out: a node that can reach nobody admits no inbound feed either, so its stamp goes stale and the verdict falls back to degraded on its own. SCOPE: it replaces a degraded verdict and only ever a degraded verdict -- unreachable with an empty failing set (so healthy is unreachable from it) and never replacing connecting (a round still working through its first dials has established nothing). Additive on the machine surface like degraded-partial before it, for the mirrored reason: an older reader meets an unfamiliar token rather than a degraded that blames the wrong machine. Gate: doc -- the ADR-0039 Decision 5 amendment naming the third word, both halves of the discriminator, the scope, and why the sequester survives; impl -- the verdict variant and its arm ordered ahead of both degraded arms, the registry per-node routability accessor whose three states keep unknown distinct from offline, the pump-side sample taken beside the admit stamp, and both render surfaces; unit -- a failing peer whose node the registry reports offline reads the new word and never healthy, ONE unaccounted failing peer holds the degraded-partial verdict (including a peer the registry merely knows nothing about), stale or never-admitted gossip forces the conservative read at the window boundary, and the arm never displaces connecting nor outlives a departed target.
2026-09-06T13:09:02.1655661Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1655690Z 
2026-09-06T13:09:02.1655807Z ### REQ-INBOUND-VERDICT-RECORD-BINDER-PINNED
2026-09-06T13:09:02.1660940Z - Title: The inbound-reachability verdict is written by the process that BINDS the UDP socket (the daemon, at endpoint bringup) into a record every renderer reads, and that record is PINNED to the binder's identity — pid AND image path — with validity RE-DERIVED from the process table at read time: the recorded pid must still resolve to an image and that image must be the recorded one, or the record is discarded as UNKNOWN. The pin is the whole point — a daemon that restarted onto a new ephemeral port, or died before re-verifying, must not leave behind a verdict that still reads reachable, because a stale green is producible by exactly the fault the verdict exists to detect (KNOWN-HAZARDS 7.61). Image path is part of the identity because a pid alone is recyclable. The pin explicitly does NOT key on <spt_home>/daemon.pid: that breadcrumb is best-effort by contract and is written BEFORE the socket bind that arbitrates single-daemon-per-home, so a losing starter overwrites the winner's value on its way out (measured on HFENDULEAM 2026-07-31 — the file named a pid dead since startup while the real daemon served on another), and a verdict keyed to it would be discarded for the daemon that actually wrote it. Absent, corrupt, dead-binder and recycled-pid are the SAME answer: unknown, never fine. Only the binder writes; the CLI renders and never probes on its own, because only the binder knows both its own image path and the port it actually got. Gate: impl — the record type, the binder-side write at daemon bringup, the process-table-validated read; unit — a dead binder reads UNKNOWN not its recorded verdict, a live pid running a different image reads UNKNOWN, absent and corrupt read UNKNOWN, a live binder round-trips its verdict.
2026-09-06T13:09:02.1661704Z - Required stages: impl, unit
2026-09-06T13:09:02.1661733Z 
2026-09-06T13:09:02.1661861Z ### REQ-INBOUND-WINDOWS-RULE-BINDER-PATH-MATCH
2026-09-06T13:09:02.1666517Z - Title: Windows inbound reachability is verified by PATH MATCH against the binder, not by rule name: the check passes only when the installer's program-scoped firewall rule names the executable of the resident daemon — the process that binds the QUIC/mDNS UDP socket — and the elevated repair rewrites the rule to THAT path. Supersedes the name-presence probe REQ-INSTALL-7 shipped, which a rule pointing at a moved or dev-build binary satisfies while inbound is blocked in fact (gap class 2). Measured on HFENDULEAM 2026-07-31: the rule names %LOCALAPPDATA%\spt-core\bin\spt.exe and the resident daemon runs that same installed binary, so this host is a MATCH, not a live mismatch — the class is proven by construction (a program-scoped rule admits exactly one path) and by unit fixture, not by a field specimen. An earlier version of this title claimed the mismatch was live here; direct measurement refuted it. The authoritative binary is never the invoking CLI's argv[0]: a repair keyed to the caller would rewrite the rule onto whichever build happened to run the command. With no resident daemon the verdict is UNKNOWN — the check reports that it cannot say rather than guessing a path. Unelevated: report loudly with the exact command, write nothing. Gate: doc — KNOWN-HAZARDS/install docs state that the rule is verified against the binder's path; impl — the netsh verbose probe, the Program-line parse, the decide seam, the elevated repair; unit — path match passes, path mismatch fails with the rule's path in the verdict, a missing Program line and a failed probe both read UNKNOWN rather than either pass or fail.
2026-09-06T13:09:02.1666644Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1666672Z 
2026-09-06T13:09:02.1666783Z ### REQ-INBOUND-LINUX-HOST-FIREWALL-VERIFY
2026-09-06T13:09:02.1670357Z - Title: Linux inbound reachability is VERIFIED and rendered, never repaired: the daemon detects the active host firewall (ufw, nftables, firewalld), decides whether inbound UDP to the port it ACTUALLY bound is permitted, and renders the exact command an operator would run. Zero writes to the host firewall. The Linux sibling of REQ-INBOUND-WINDOWS-RULE-BINDER-PATH-MATCH, split on the platform axis and stopping short of repair for a stated reason: a durable rule needs a stable port and production binds ephemeral (BindScope::Port is test-scope only), so an auto-written rule would be stale at the next restart while reading repaired. Auto-repair is relocated to releases#54 behind the pinned-port decision, not dropped — see REQ-NET-PINNED-UDP-PORT. No detected firewall is not a pass: it is UNKNOWN unless the probe positively determined the port is reachable. Gate: doc — the install docs state the Linux posture (verify + tell, no writes) and name the command; impl — the bounded ufw/nft/firewalld probes and the decide seam over (firewall kind, ruleset, bound port); unit — an allowing ruleset passes, a blocking one fails carrying the rendered command, an absent/failed probe reads UNKNOWN.
2026-09-06T13:09:02.1670676Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1670704Z 
2026-09-06T13:09:02.1670847Z ### REQ-INBOUND-PROVIDER-FIREWALL-DOCS
2026-09-06T13:09:02.1672980Z - Title: Provider-level firewalls (cloud vendor security groups — the DigitalOcean droplet case) are unreachable by any installer BY DESIGN, so inbound UDP for QUIC is a stated hosting REQUIREMENT in the public docs rather than something code pretends to fix. The docs leg exists because the failure is otherwise indistinguishable to the operator from the host-firewall classes REQ-INBOUND-WINDOWS-RULE-BINDER-PATH-MATCH and REQ-INBOUND-LINUX-HOST-FIREWALL-VERIFY cover: same silent NO_SEED_HOLDER symptom, a cause no local probe can see. Gate: doc — a public-docs section naming the inbound-UDP requirement, that it is the operator's to satisfy at the provider, and the symptom it produces when unsatisfied.
2026-09-06T13:09:02.1673147Z - Required stages: doc
2026-09-06T13:09:02.1673176Z 
2026-09-06T13:09:02.1673284Z ### REQ-NET-PINNED-UDP-PORT
2026-09-06T13:09:02.1675120Z - Title: Seed (releases#54, doyle L3 ruling 2026-07-31): a pinned UDP port for the production QUIC endpoint, so a host-firewall rule can be written once and stay true across restarts. Today BindScope::All binds ephemeral and BindScope::Port exists only in test scope (pairhost maps it back to All), which is why REQ-INBOUND-LINUX-HOST-FIREWALL-VERIFY ships verify-only — the repair half has nothing durable to allow. Not a rider on the verify wave: pinning a port is a transport-config AND security-posture decision with pairing/ticket/two-host ripple, and it is operator-visible, so it takes its own wave and its own gate.
2026-09-06T13:09:02.1675234Z - Required stages: 
2026-09-06T13:09:02.1675262Z 
2026-09-06T13:09:02.1675363Z ### REQ-ACL-SURFACE-ATTRIBUTABILITY
2026-09-06T13:09:02.1679729Z - Title: Whether a control surface's inbound carries a PROVEN sender endpoint is single-sourced per-surface metadata, never a hardcoded MSG-vs-rest split. Attributability decides which SUBJECT TIER a grant for that surface can actually bind: attributable => a sender-endpoint subject can match (today MSG, via the W2b daemon stamp); non-attributable => only node-tier subjects are real, and a sender-endpoint entry on such a surface is dead on arrival — it can never match, so writing one is a silent no-op the CLI must refuse rather than persist. The single-source form is a per-surface table carrying the flag beside the id, with the v1 id list DERIVED from that table and exactly one reader answering the question; a second site that re-derives 'MSG or not' is the drift class this requirement exists to prevent (a property matched at one site and built at another). The payoff is automatic: when a surface's records grow their own sender stamp (XFER is the expected next), flipping one row makes it attributable and it exits every restriction keyed on this property with no re-ruling and no second edit. Gate: doc — the CONTEXT.md sender-attributability entry; impl — the per-surface table with the derived id list and the single is_attributable reader; unit — the derived list matches the table exactly, MSG reads attributable and a non-stamped surface does not, and flipping one row's flag moves that surface across every attributability-keyed decision without touching another site.
2026-09-06T13:09:02.1679996Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1680025Z 
2026-09-06T13:09:02.1680119Z ### REQ-ACL-ORIGIN-QUALIFIER
2026-09-06T13:09:02.1686433Z - Title: An access rule may carry an ORIGIN QUALIFIER (any|user|agent) restricting it to invocations of that origin class, so a rule can say 'the humans on node A, not its agents' and let that node's agents fall through to the mode. TWO TYPES, NOT ONE (doyle-ruled design): the RULE side is a three-valued qualifier defaulting to `any` for N-1 records; the REQUEST side is a TOTAL two-valued class (user|agent) with no unknown arm, because `any` is a rule-side value that no classifier ever emits — a single three-valued type would let a classifier return `any`, which is meaningless. Classification is ambient and ceremony-free, performed by the SENDING daemon over the existing local-origin predicate (interactive terminal with no perch/broker ancestry => user; agent-session ancestry => agent) and carried to the receiving gate as a daemon-stamped sibling of the sender-endpoint stamp — the same shape of fact, so the same seam, never a parallel path. ABSENT OR UNCLASSIFIABLE MAPS TO agent AT THE RECEIVING BOUNDARY: the restrictive class, so a `user` rule never admits an unknown and N-1 senders land safe automatically without a compat flag. Honest limit, stated and never to be inflated: this is the honest member daemon's report — sound against agents on honest nodes (ancestry cannot be env-scrubbed away), NOT against a malicious node, and a detached process spawned outside an agent's session tree can launder to `user`. Defense-in-depth, not proof; a forged origin stamp sits at the same trust tier as a forged sender stamp and never elevates the subject chain. The `user-proven` TOTP-minted pid-linked token is FORWARD DESIGN and must not be built. Gate: doc — the CONTEXT.md origin-qualifier entry plus the amended wire-module identity-field doc carrying origin's forgery blast radius; impl — the two types, the rule field with its serde default, the request field, the chain's qualifier check, and the sending-daemon stamp; unit — the qualifier matches its own class and abstains on the other, `any` matches both, an absent request-side stamp evaluates as agent, an N-1 rule record with no qualifier decodes as `any`, and the wire field is additive in both directions exactly as the sender stamp is.
2026-09-06T13:09:02.1686611Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1686640Z 
2026-09-06T13:09:02.1686733Z ### REQ-KNOCK-VERBS
2026-09-06T13:09:02.1690660Z - Title: Knocking is one origin-classified verb family under `spt knock ...` — no user-flavored parallel commands and no second namespace. Agent-invoked, the knocker is the session-proven self endpoint and is never caller-supplied; user-invoked from a bare terminal with no perch, the knock requests the deliberately NARROW `subject <invoking-node>, origin user` ('the humans on my node') rather than a bare node entry, so the commonest approval admits humans only, with an explicit flag to knock on behalf of a local endpoint the sending daemon VERIFIES exists. Target-side minting of an invite code is self-sovereign for an endpoint's own agent; the node-target form is engine-room-only. Every form's stdout states plainly what was created, granted or activated — subject, surfaces, scope, expiry — because the grant a user cannot read is the grant they did not mean. Gate: doc — the CONTEXT.md knock-initiation entry and the public CLI reference; impl — the verb family with invocation-time origin classification and the local-endpoint verification; unit — an agent invocation stamps itself and cannot supply another knocker, a bare-terminal invocation produces the narrow node+user subject rather than a bare node subject, an unknown local endpoint is refused at send, and every form's stdout names subject, surfaces and scope.
2026-09-06T13:09:02.1690885Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.1690914Z 
2026-09-06T13:09:02.1691061Z ### REQ-KNOCK-UNRESOLVED-REFUSES
2026-09-06T13:09:02.1698912Z - Title: A KNOCK THAT RESOLVED NOWHERE REFUSES LOUDLY AND RECORDS NOTHING, and a knock that lands in the sender's OWN inbox says so. Operator-ruled 2026-08-03 (releases#145): if `spt knock` cannot reach endpoints on nodes lacking a DISCOVER rule, it must directly report that it cannot find the endpoint. The defect was one site with two halves. First, the wire leg's NotFound outcome fell through to the LOCAL landing unconditionally, ten lines under a comment naming the exact hazard it then committed — 'a local-only record would leave the operator believing they asked when nobody was told' — so a target that existed on no reachable node was written into the SENDER's own store and reported with the fully positive 'It is waiting in their inbox', exit 0. Field case: ball-b@ENLYZEAM knocked lia@HFENDULEAM and got a positive stdout while lia's node held zero knock rows; both nodes were members of one CLOSED subnet, where endpoint-tier discovery is deny-default BY DESIGN, so resolution NotFounded correctly and the whole defect is the CLI's answer to it. Second, the LOCAL and remote-QUEUED landings rendered an IDENTICAL closing sentence, so the operator could not tell 'their node has it' from 'this machine recorded it in hope'. THE LOCAL LANDING SURVIVES ONLY FOR A TARGET VERIFIED TO EXIST HERE — it exists for a LOCAL endpoint carrying no registry row (unregistered or asleep), and the question that separates that from a target existing nowhere is whether the target is an endpoint on this machine, asked with the same predicate `--for` already asks of the knocker. THE REFUSAL NAMES THE REMEDY THAT ACTUALLY REACHES AN UNDISCOVERABLE ENDPOINT: the knock-code (mint on their machine, redeem here), because in a closed subnet the bare knock is not merely failing, it is the wrong mechanism — and the prescribed invocations must be forms the binary HONORS (REQ-KNOCK-PRESCRIBED-REMEDY-HONORED applies to a refusal's advice exactly as it applies to a listing's). Gate: impl — the landing decision as a PURE classifier over the outcome plus the target's locality, the locality predicate, and one composer rendering every landing's line; unit — a NotFound at a non-local target refuses while a NotFound at a local endpoint still lands, the landing classifier and the wire-refusal renderer partition the outcome enum with no variant covered twice or missed, and the three landing lines are RENDERED and asserted distinct (the identical-sentence half is invisible to any test that does not render); int — the real binary knocking a target that exists nowhere exits nonzero, names KNOCK_NOTFOUND and leaves the store with no row, beside a local endpoint on the same home landing and exiting 0 so the refusal is discriminating rather than a verb that always refuses.
2026-09-06T13:09:02.1699188Z - Required stages: impl, unit, int
2026-09-06T13:09:02.1699217Z 
2026-09-06T13:09:02.1699312Z ### REQ-KNOCK-KNOCKER-STAMP
2026-09-06T13:09:02.1702671Z - Title: BINDING: the knock record's knocker field is daemon-stamped at mint (session-proven, the W2b discipline) and never caller-supplied. This is the load-bearing security property of the whole knock mechanism, not a hardening detail — an approval WRITES A WHITELIST ENTRY FOR THAT ID, so a forgeable knocker field is a grant-to-attacker primitive: an attacker who can name someone else as the knocker gets the target's own agent to authorize reach for an identity the attacker controls. KNOCK is therefore attributable by construction, independent of the per-surface attributability of whatever surfaces the knock requests. The stamp is applied at the same sending-daemon seam that proves the sender endpoint, and no CLI flag, envelope field or record edit may override it after mint. Gate: doc — the CONTEXT.md knock entries naming the stamp as binding; impl — the mint-time stamp with no caller-supplied path to the field; unit — a knock minted by an agent carries that agent's proven id, a caller-supplied knocker value is ignored or refused rather than persisted, and an approval writes its entry for the STAMPED id and never for a supplied one.
2026-09-06T13:09:02.1702922Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.1702951Z 
2026-09-06T13:09:02.1703058Z ### REQ-KNOCK-AUTHORITY-SPLIT
2026-09-06T13:09:02.1708043Z - Title: Who may answer a knock, or mint an invite code, follows the TARGET TIER of what it grants. Node-target grants (node-tier rules, node modes, any permission no single endpoint owns) are answerable and mintable ONLY by the node's engine-room and land only in the engine-room's inbox. Endpoint-target grants stay SELF-SOVEREIGN — the target endpoint's own agent answers — with one guard: approving a surface that is not sender-attributable writes a NODE-SUBJECT entry, admitting every endpoint on the knocker's node whatever the knock named, so that approval (and a code-mint baking such a surface) must carry an explicit widening acknowledgment at the CLI and is gated by the node's grant-nodes policy. No silent widening, ever. A code is a pre-approval, never a widening loophole: it carries the same acknowledgment as the approve it stands in for. Where a single knock mixes attributable and non-attributable surfaces under a false policy, the attributable SUBSET is approvable and the refused remainder STAYS PENDING with stdout naming the engine-room as the seat that can answer it — the mixed case renders its own verdict and must not collapse into a blanket approve or a blanket deny (hazard 7.61: a partial fault renders its own verdict). Gate: doc — the CONTEXT.md grant-authority entry; impl — target-tier routing, the widening acknowledgment, and the partial-approve path; unit — a node-target knock reaches only the engine-room inbox, an endpoint-target knock is answerable by its own agent and not routed to the engine-room, a non-attributable approval without the acknowledgment is refused, and a mixed knock under a false policy grants the attributable subset while the remainder stays pending naming the engine-room.
2026-09-06T13:09:02.1708203Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1708241Z 
2026-09-06T13:09:02.1708337Z ### REQ-ACL-LOCKED-POSTURE
2026-09-06T13:09:02.1719623Z - Title: The **locked** claim is one predicate, and it asks the CHAIN. `ACCESS_LOCKED` (the `revoke` arm) and the access view's `locked` field are two renderings of a single fact — this endpoint refuses ALL unsolicited remote inbound — and both take that fact from ONE predicate, `AccessStore::is_locked(endpoint)`. It is CHAIN-SCOPED by necessity, not by preference: the superseded `EndpointAcl::is_locked` was record-scoped and therefore structurally unable to answer, because an endpoint that names no posture of its own inherits one from the node mode and from captured subnet modes, neither of which a record can see; it counted node-subject allows (`allowed_nodes().is_empty()`) and never read a mode at all, so a `revoke` on a default-open endpoint told the operator their endpoint now refuses all remote inbound while the chain admitted everyone, and `--json` said `locked: true` about a store the human view correctly rendered as open in the same breath (releases#206). LOCKED MEANS, for every surface in the set: the effective posture is closed AND no ALLOW rule at endpoint-or-node scope covers it. BOTH halves are load-bearing — posture alone re-mints the same false sentence in mirror image, since rules outrank modes (tiers 1-5 above 6-8) and a closed endpoint carrying one allow row still admits somebody. Only ALLOW rows are holes (a deny row is the posture agreeing with itself), an allow qualified by origin class is still a hole because it still admits somebody, and the hole-scan reads surface coverage through the chain's OWN matching machinery rather than re-deriving what covers means — a second reading of rule semantics is precisely how the two renderings drifted apart. THE SET IS DERIVED, never enumerated: every non-default-on row of the surface table, so a later wave's surface joins the claim by the sole act of existing, and the derivation is proven against a FIXTURE table whose default-on row is not DISCOVER (with `TABLE` a const, a reader hardcoded to all-but-DISCOVER agrees with every shipped row and no test over `TABLE` can tell the two apart). DISCOVER — the one default-on row — is deliberately OUTSIDE the predicate: a blanket `closed` never GOVERNS it (releases#180), so a literal reading of the sentence would be false for every blanket-closed endpoint in the fleet and the line could never truthfully fire at all. That fact is carried BESIDE the sentence by `discover_still_open_line`, required ADJACENT IN THE SAME EMISSION wherever the locked claim prints, sibling-arm precedent: the sentence alone is the overclaim, sentence-plus-caveat is the honest composite, and the `ACCESS_LOCKED` wording itself is unchanged under this shape. The JSON view exposes the same facts from the same predicate and the same composer data, so the machine and human accounts cannot disagree. Scope of the claim is REMOTE — same-node reach and the reply exemption lie outside what the sentence asserts. Separately but in the same lane: `AccessStore::revoke` prunes a record left carrying neither rules nor modes, as `apply_mutation` already does on a Remove, because `is_vacant`'s own doc names a vacant record as the shape that must not persist — it read as restricted through the old predicate. Gate: doc — the CONTEXT.md locked-claim entry; impl — the one chain-scoped predicate, the derived surface set, the chain-machinery hole-scan, the adjacent discover caveat at every emission site, and the vacant prune in revoke; unit — a revoked default-open endpoint is not locked, a node-wide close locks an endpoint that names no posture, a closed endpoint with an allow hole is not locked while a deny row does not unlock, an origin-qualified allow still counts as a hole, a node-scope allow holes an endpoint's lock, DISCOVER stays outside the claim while a fully closed endpoint still reads locked, the surface set derives from a fixture table carrying an invented row, and revoking the last rule leaves no vacant record.
2026-09-06T13:09:02.1719956Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1719984Z 
2026-09-06T13:09:02.1720088Z ### REQ-ACL-GRANT-NODES-POLICY
2026-09-06T13:09:02.1724746Z - Title: ENDPOINTS_CAN_GRANT_NODES is a node-tier policy toggle, settable only via the engine-room, gating whether an ordinary endpoint may write a NODE-SUBJECT entry into its own whitelist — the widened grant a non-attributable-surface approval produces. Explicitly set it is true or false; UNSET DERIVES PER-DECISION from the effective posture of the REQUESTED SURFACE at the target node: open => endpoints may self-approve (an allow entry on an open surface punches no hole), closed => engine-room only (a node-subject allow would punch a hole in a closed posture). The gate applies in BOTH POLARITIES — adding a node allow and removing a node deny are the same widening act — and it is enforced at the STORE MUTATION SEAM rather than in any one caller, so neither the CLI nor the knock-answer path can bypass it. It is distinct from the resolution chain's node-scope TIER: per-node chain entries are engine-room-owned always, policy or no policy; this toggle governs only the SUBJECT side of self-sovereign whitelists. The engine-room is not bound by it — its bring-up gate is the authorizing ceremony. Name caveat accepted at mint: 'grant nodes' means node-SUBJECT grants, not node-tier rules. Gate: doc — the CONTEXT.md policy entry; impl — the toggle, the unset-derivation from the requested surface's effective posture, and the both-polarity check at the mutation seam; unit — explicit true and false both honored, unset derives open-permits and closed-refuses from the requested surface, removing a node deny is gated exactly as adding a node allow, and neither the CLI nor the knock-answer path can reach a widening write without passing the seam.
2026-09-06T13:09:02.1724941Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1724970Z 
2026-09-06T13:09:02.1725056Z ### REQ-ACL-RULE-MUTATION
2026-09-06T13:09:02.1731818Z - Title: Access rule mutation and revocation are TUPLE-SHAPED, never id-shaped: a removal restates the subject, surfaces and origin qualifier rather than naming a generated rule id, which makes it idempotent and script-safe and removes rule-id bookkeeping from the model entirely (tuples ARE the identity). Every entry records its PROVENANCE — knock-approve, code-redeem, or manual — and the access drill-down view prints the exact removal command beside each rule together with that provenance, so revocation is copy-paste from the view that showed it. Entries born of knocks or codes have no special lifecycle after birth. Authority is threefold: the owner endpoint's agent (narrowing mutations always free, widening node-subject mutations in either polarity gated by the grant-nodes policy); same-node users always (node-sovereign, the emergency lever against a bad agent grant — there is no remote rule mutation of any kind, the remote-human path is to rc into the node); and the engine-room, which may edit node-tier entries, modes AND endpoint-scope entries across the node as the node's admin command center for access. That engine-room rule-EDITING authority supersedes the earlier 'engine-room owns modes only' clause; knock-ANSWER routing is unchanged — endpoint-target knocks still land with, and are answered by, the target endpoint. Every mutation path loads the store through the FAIL-CLOSED reader: a degraded store refuses the mutation loudly, leaves the store bytes byte-identical, and — where the mutation was a knock answer — leaves the knock PENDING rather than consuming it, because a refused write must never burn a single-use answer. Gate: doc — the CONTEXT.md rule-mutation entry and the CLI reference; impl — the tuple-matching removal primitive, the provenance field, the view's per-rule removal command, and the fail-closed load on every mutation path; unit — a tuple removal is idempotent and removes only the matching tuple, provenance survives a round-trip and renders in the view, the printed removal command actually removes the rule it was printed beside, a narrowing mutation needs no acknowledgment while a widening one does, and a mutation against a degraded store refuses loudly with the file bytes unchanged and any answering knock still pending.
2026-09-06T13:09:02.1732050Z - Required stages: 
2026-09-06T13:09:02.1732078Z 
2026-09-06T13:09:02.1732163Z ### REQ-KNOCK-FLOOD
2026-09-06T13:09:02.1737651Z - Title: Knocking cannot be used to flood, enumerate or brute-force. One pending knock per (knocker, target) pair — a re-knock UPDATES IN PLACE rather than queueing a second row, so an inbox cannot be filled by repetition. Knocks carry a TTL of one day and the expiry is enforced AT THE ANSWER SEAM: an expired knock is unapprovable regardless of what a stale list rendering showed, because a TTL that only filters a view is a display convention rather than a boundary. Inbound knocks are rate-limited per node (roughly ten an hour by default, knob-adjustable) using the existing windowed-counter shape rather than a newly minted clock pattern, and code redemption attempts are rate-limited per node likewise. Redemption is single-use and ATOMIC — two concurrent redemptions of one code yield exactly one success — and a redemption whose resulting write is refused does NOT burn the code. Code entropy is sized so guessing is closed under the TTL and rate limit together, and codes carry their target so no blind enumeration is possible through the discovery bypass (the docs state this). An expiry that removes a knock from the inbox must render as expired rather than as a clean nothing-to-approve: set-membership change is a re-entry path for hazard 7.61 and is pinned by test. Gate: doc — the CONTEXT.md knock entry stating TTL, one-pending-per-pair and the enumeration property, plus the public docs note on codes carrying their target; impl — the in-place upsert, the answer-seam expiry check, the per-node rate windows, and atomic single-use redemption; unit — a re-knock updates rather than appends, an expired knock is refused at answer even when a stale list showed it pending, concurrent redemptions produce exactly one success, a refused write leaves the code unburned, the rate limit refuses past its window and admits within it, and an expired-out knock renders as expired rather than as absence.
2026-09-06T13:09:02.1737890Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1737918Z 
2026-09-06T13:09:02.1738008Z ### REQ-KNOCK-MUTUAL
2026-09-06T13:09:02.1745895Z - Title: Two-way reach is ENCOURAGED but never imposed, and NOBODY WRITES ANOTHER ENDPOINT'S RULES — the principle this requirement exists for, unchanged through two amendments of its mechanism. AMENDED 2026-08-04 (USHER, releases#144 / ADR-0055), CORRECTING the 2026-08-01 DOORBELL W2 amendment rather than adding to it: two-way reach DECOMPOSES INTO TWO SOVEREIGN ACTS, each side opening only its own inbound, and each act is declared by the side that PERFORMS it. The requester declares theirs at the requesting verb (--send-receive pre-authorizes a rule on the requester's OWN endpoint, written by their own daemon at answer-receipt, keyed to the knock's correlation id and the node-proven answer, subject exactly the endpoint knocked, and CONSUMED EXACTLY ONCE: a replayed or duplicated answer must not write it twice, and an answer that never arrives leaves it unconsumed rather than dangling as an open grant). The RECEIVER declares nothing when accepting — accepting IS their own-side act — and REVERSE REACH IS ARMED BY THE OTHER SIDE'S OWN KNOCK-BACK, an ordinary knock carrying its own declaration, never a rider on an approval and never a grant written across the boundary. SUPERSEDED SPELLINGS, both of which this requirement once carried: "one-directional approve stands as the default" (a silent default nobody typed), and "approving with the mutual flag performs approve-plus-counter-knock" (a second grammar for the knock verb's own job — removed, not renamed, by ADR-0055). What survives every revision intact: auto-mutual stays BANNED, and so does a standing "mutual mode" setting. The forced-choice machinery now lives at the requesting seats and is specified by REQ-KNOCK-DIRECTIONALITY-SENDER. NOTIFICATIONS ARE NOW TWO, not three, and the reduction is a CONSEQUENCE of the counter-knock's removal rather than a separate decision: an approval notifies the knocker, and a code redemption notifies the code's minter. The third — a counter-knock's arrival notifying the original knocker — was produced by the counter-knock path and dies with it; a knock-back is an ordinary knock and lands inbox-only, which is the never-auto-surface default the other two are exceptions to. Nobody whose own act did not invite the notice is ever notified. Gate: doc — the CONTEXT.md mutual-whitelisting entry, carrying the two sovereign acts and the knock-back as the reverse's only instrument; impl — the pre-authorization record with its correlation-id keying and once-only consumption, and the two notifications; unit — a pre-authorization is consumed exactly once under a replayed answer, an unanswered knock leaves it unconsumed, the send-only stdout prints a reverse command that actually PARSES under the mandatory-flag grammar, and no third notification path exists.
2026-09-06T13:09:02.1746080Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1746110Z 
2026-09-06T13:09:02.1746209Z ### REQ-ACL-SURFACE-DESCRIPTION
2026-09-06T13:09:02.1751836Z - Title: Every control surface carries its operator-language DESCRIPTION as a third column of the same single-source surface table that already carries its id and its attributability, and each description states that surface's SUBJECT CONSEQUENCE rather than merely naming the traffic: an attributable surface says a grant binds the single proven sender ('MSG — direct messages (a grant binds the single sender)'), a non-attributable one says a grant admits the whole machine, and STOPS THERE: the remedy stays sited with the flag that owns it (the --admit-node help on knock approve and on endpoint access allow|deny|remove, plus the store's write-time refusal), because a second remedy spelling in a section that renders at a dozen sites is a drift pair — and would be outright FALSE at daemon access, a seat that acknowledgment does not bind ('XFER — file transfer (a grant admits the whole machine)'). Consequence, not category, is the load-bearing half: an operator choosing surfaces is choosing WHO gets admitted, and a description that omits that is decoration. The text is composed into every rendering AT RENDER TIME from the table, never restated at a display site, so the vocabulary a human reads and the vocabulary the gate enforces cannot drift apart and A NEW SURFACE APPEARS IN THE RENDERED HELP BY THE SOLE ACT OF EXISTING IN THE TABLE — that property is the point of the requirement and is proven directly rather than inferred from the shipped rows, which are uniform in it. Gate: doc — the CONTEXT.md surface-vocabulary entry carrying the consequence rule; impl — the description column beside the id and attributability flag, plus the one composer every renderer calls; unit — a fixture table carrying an INVENTED row renders that row with no edit to any display site, an attributable and a non-attributable row render their differing subject consequences, and the composed text is byte-derived from the table rather than matched against a second literal.
2026-09-06T13:09:02.1752131Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1752156Z 
2026-09-06T13:09:02.1752257Z ### REQ-CLI-SURFACE-SECTION-SITED
2026-09-06T13:09:02.1758442Z - Title: The composed `Control surfaces:` section rides the --help of exactly the verbs whose invocation NAMES A SURFACE, and of no others, so the vocabulary meets the operator at the moment of choosing rather than in a manual: `knock` bare, `knock send`, `knock approve`, `knock new-code`, `knock list`; `endpoint access allow` / `deny` / `remove` and the FLAGLESS `endpoint access` listing (which is an EndpointCmd variant, not an AccessCmd one — a distinct site that a subcommand-only sweep silently misses); `daemon access` and its `allow` / `deny` / `remove`; and `subnet create`. DELIBERATELY EXCLUDED and recorded as decided, not overlooked: `knock redeem` and `knock deny` (the surfaces are already fixed by the code or the knock — printing the table there invites the reader to believe they have a choice), and `endpoint fork`, which keeps its existing INLINE FORK+DISCOVER prose because a two-surface verb is better served by a sentence than by a table. The section is appended through clap's after-help placeholder, which the tree uses nowhere today — a NEW PATTERN, so the root help template's existing but unused placeholder is what carries it, and the section text itself comes from the surface table's composer rather than from any per-verb literal. Public help stays free of internal codes at all depths, including the three-deep sites the docs-site drift gate cannot reach. Gate: doc — the public CLI reference; impl — the after-help wiring at each named site, fed by the single composer; unit — every named verb's rendered help carries the section and every excluded verb's does not, the flagless listing site is covered distinctly from its subcommands, the rendered text matches what the composer produced for the same table, and A WALK OVER EVERY NAMED SITE'S RENDERED HELP ASSERTS NO INTERNAL CODE (REQ-, ADR-, F-) APPEARS AT ANY DEPTH — the row that closes the three-deep gap the docs-site drift gate cannot reach, and which earns its green two ways rather than by default: the walk's site set is PINNED (it asserts it visited exactly the named sites, so an empty or truncated enumeration fails loudly rather than passing vacuously) and the same predicate is proven ABLE TO FAIL against a fixture help text carrying a code.
2026-09-06T13:09:02.1758566Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1758681Z 
2026-09-06T13:09:02.1758780Z ### REQ-CLI-MONIC-TRIGGER-SECTION
2026-09-06T13:09:02.1765402Z - Title: `spt endpoint monic --help` carries a composed `Trigger kinds:` section naming every kind a trigger can watch, WHAT it watches, and WHETHER ANYTHING EVALUATES IT TODAY — the third column being the load-bearing one, because `user-input` and `agent-output` are ratified but INERT, and a section that listed them beside the live kinds without saying so would present a trigger that can never fire as one that works. That is the same honesty `monic list` already owes a stored trigger, now owed at the moment of CHOOSING one. The section is composed AT RENDER TIME from a single row table whose row carries the `TriggerKind` ITSELF, so the kind's wire spelling and its evaluated-today answer are DERIVED — from the enum and from the existing `has_live_consumer` predicate — rather than restated beside them: a kind cannot appear in the help under a spelling the `--triggers` parser would reject, nor be described as evaluated when nothing evaluates it, and a kind added to the vocabulary reaches the help by the sole act of being in the table. PLAIN TEXT, no inline Markdown, for the reason the surface section carries the identical rule: the CLI renders help through `helpfmt`, and a composer in the store crate can be read by a consumer that never passes through that renderer, so a Markdown marker reaches one audience styled and the other as literal asterisks. The words match the published docs table (kind | watches | evaluated today) rather than paraphrasing it, so an operator who read the manual and an operator who read the help learned the same vocabulary. Gate: doc — the docs-site trigger-kind table these rows speak in the same words, and the generated public CLI reference; impl — the row table beside the enum it describes, the render-time composer taking the table AS A PARAMETER, and the after-help wiring at the sited verb; unit — a fixture table carrying an INVENTED row renders that row with no edit to any display site (table-drivenness proven rather than inferred from a snapshot whose rows all agree), EVERY enum variant is present in the table (a kind added to the vocabulary cannot be silently missing from the section), a live and an inert row render their differing third column, the rendered help at the sited verb carries the composer's own text byte-for-byte rather than a second literal, and that help leaks no internal code.
2026-09-06T13:09:02.1765622Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1765646Z 
2026-09-06T13:09:02.1765745Z ### REQ-ER-BRIEFING-SURFACE-VOCAB
2026-09-06T13:09:02.1771541Z - Title: The engine room is made aware of the surface vocabulary in the same words the CLI teaches it: the surface section is composed FROM THE SAME TABLE AND THE SAME COMPOSER the CLI help uses, WHEREVER THE ENGINE ROOM RECEIVES IT, rather than restating the vocabulary in prose. AMENDED BY REPLACEMENT in the KEYSTONE #182 W3 lane (doyle ruling on releases#179, 2026-08-19): the surface section moved from the bring-up briefing to the engine room's durable in-core role (REQ-ER-ROLE-STATIC-IMMUTABLE), so this requirement names the PROPERTY rather than one carrier — single-source is preserved across the carrier move, and role, briefing and CLI help remain unable to diverge. Before this section existed the engine room saw surfaces only as the SURFACE column of the ruleset table — a rendering of what rules already say, which tells an engine room what is configured but never what the words mean or what granting one costs. The single-source property IS the requirement: an engine room taught at seat time and an operator reading --help must be unable to receive different vocabularies, and a surface added to the table must reach BOTH renderings with no second edit — which is why the composer is table-PARAMETERIZED, and why a hardcoded surface list, in the role or anywhere else, is a defect this requirement refuses (the carrier of the role is a function and not a const for exactly this reason). The engine room must also not be sent to a verb that no longer exists: a section that teaches the vocabulary while prescribing a dead command teaches half a lie. Gate: doc — the CONTEXT.md engine-room entry naming the surface section as table-composed wherever it is served; impl — the role composer and the briefing both calling the shared composer, never a literal surface list; unit — a fixture table carrying an INVENTED row reaches the engine room exactly as it reaches the help, the section text is identical to the CLI's for the same table, and no prescribed subcommand is one the CLI does not parse.
2026-09-06T13:09:02.1771789Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1771818Z 
2026-09-06T13:09:02.1771917Z ### REQ-KNOCK-BARE-VERB
2026-09-06T13:09:02.1775638Z - Title: `spt knock <target>` is the bare form of the knock verb — sugar routing to `knock send` with identical semantics, identical origin classification and identical defaults, never a second code path with its own behavior. The docs already describe this form (the CLI's own knock doc-comment and the CONTEXT.md knock-initiation entry both state it): this requirement makes those sentences TRUE rather than amending them away, which is the correct repair for a documented-but-unbuilt affordance. Because clap resolves a subcommand name before a positional, an endpoint whose id collides with a subcommand name (`send`, `list`, `approve`, `deny`, `new-code`, `redeem`) is shadowed and must be knocked as `knock send <id>`; that SHADOWING RULE IS DOCUMENTED where an operator meets it rather than left to be discovered, since the failure mode is a knock that silently runs a different verb. Gate: doc — the CONTEXT.md knock-initiation entry and the public CLI reference stating the bare form and the shadowing rule; impl — the optional-subcommand-with-positional shape routing to the send path; unit — the bare form produces the same knock the explicit send form produces for the same arguments, and a target whose id equals a subcommand name resolves to THAT SUBCOMMAND (the precedence is pinned, not assumed).
2026-09-06T13:09:02.1775804Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1775833Z 
2026-09-06T13:09:02.1775929Z ### REQ-KNOCK-DEFAULT-SURFACE-MSG
2026-09-06T13:09:02.1780078Z - Title: A knock sent with no --surfaces requests MSG AND NOTHING ELSE, because the commonest knock is 'let me talk to you' and a default that requests every surface asks an operator to approve a machine-wide grant they never intended. The narrowing lives ENTIRELY IN THE CLI, which materializes an EXPLICIT [MSG] list at compose time: the store's wire encoding is UNCHANGED and empty still means every surface, so `--surfaces ALL` keeps mapping to the empty vec through the same field and an N-1 record with an empty list still reads as all. DISCOVER is deliberately OUT of the default (ruled): reachability and enumerability are different asks. SCOPE IS send AND THE BARE FORM ONLY — `approve` and `new-code` keep their current defaults, so any helper shared with them must not be changed underneath them; a narrowing applied at the shared helper would silently re-scope two other verbs. The help sentence states the affordance positively ('only use --surfaces if you need more than MSG') rather than describing the encoding. Gate: doc — the CONTEXT.md knock-initiation entry stating the send-side default; impl — the CLI-side materialization at the send/bare compose site only; unit — a bare send composes exactly [MSG], ALL composes the empty vec and still reads as every-surface, approve and new-code defaults are unchanged by the same call, and a store fixture carrying an empty list still evaluates as every surface.
2026-09-06T13:09:02.1780197Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1780225Z 
2026-09-06T13:09:02.1780345Z ### REQ-KNOCK-DIRECTIONALITY-EXPLICIT
2026-09-06T13:09:02.1790304Z - Title: RETIRED 2026-08-04 (ADR-0055, releases#144 bag grill) — SUPERSEDED BY REQ-KNOCK-DIRECTIONALITY-SENDER, which owns the surviving fact at its new seats. The forced explicit choice was RIGHT and is kept; the SEAT was wrong. Directionality is sender-declared, so the mutex moved to `spt knock` / `knock send` / `knock redeem` and left `approve` and `new-code` declaring nothing, and the mint-side pre-authorization this requirement specified is removed with the flag that armed it. Retained in the registry rather than deleted so the reversal reads back. The superseded text follows verbatim. Answering a knock and minting a code both FORCE AN EXPLICIT DIRECTIONALITY CHOICE: `knock approve` and `knock new-code` require exactly one of --mutual or --one-way, and a bare invocation REFUSES LOUDLY naming both options rather than proceeding on a silent default. A mandatory choice is not auto-mutual — nobody's rules are written without their own act — it removes the default whose consequence an operator most often failed to notice. --one-way is the explicit decline and is RECORDED (on the answer for approve, on the code record at mint for new-code) so the declining act is legible afterward, not merely an absence. `new-code --mutual` is a MINTER-LOCAL persisted pre-authorization of the REVERSE grant, armed at mint and keyed to the CODE ID, consumed EXACTLY ONCE at receipt of the node-proven REDEEMED outcome, at which point the minter's own node writes its whitelist entry naming the redeemer. There is no counter-knock at mint — at mint there is no knocker yet, which is precisely why arming defers to redemption — and this is approve --mutual's consent shape transposed from knock correlation to code id, SYMMETRIC with redeem --mutual: each side arms only its OWN side, neither ever writes on the other's node, and full mutuality requires both. The pre-authorization record is the EXISTING one, extended with an EXPLICIT TWO-VALUED KEY KIND (knock | code) rather than overloading its knock-id field with a code id: consumption is a string match, so an overloaded field makes a code pre-authorization consumable by a knock answer that merely carries the same id string, and a discriminant makes that unrepresentable instead of merely improbable. The discriminant is additive with a serde default of knock — the only kind shipped records can be — so N-1 records decode unchanged, and the code record's directionality field is additive by the same discipline so a code minted before the field existed still reads. Gate: doc — the CONTEXT.md mutual-whitelisting entry, amended so the forced choice replaces the one-directional default while the auto-mutual ban survives intact; impl — the mandatory-one-of arg shape on both verbs, the --one-way record, and the code-id-keyed pre-authorization with its once-only consumption at node-proven redemption; unit — a bare approve and a bare new-code each refuse naming both options, a mint with --mutual plus a node-proven redemption writes the minter-side entry exactly once, a SECOND redemption notice finds the pre-authorization already consumed, --one-way writes nothing on redemption, a forged or unproven redemption notice consumes nothing, A CODE PRE-AUTHORIZATION IS NOT CONSUMABLE BY A KNOCK ANSWER CARRYING THE SAME ID STRING (the row that fails under an overloaded key), and an N-1 record with neither the key kind nor the directionality field still decodes.
2026-09-06T13:09:02.1790624Z - Required stages: 
2026-09-06T13:09:02.1790653Z 
2026-09-06T13:09:02.1790762Z ### REQ-KNOCK-DIRECTIONALITY-SENDER
2026-09-06T13:09:02.1799264Z - Title: DIRECTIONALITY IS SENDER-DECLARED, AND ONLY SENDER-DECLARED (ratified 2026-08-04 bag grill, releases#144 / ADR-0055). The reach-REQUESTING surfaces — `spt knock <target>`, `spt knock send` and `spt knock redeem` — take a MANDATORY, MUTUALLY EXCLUSIVE pair, --send-only | --send-receive, with NO default: a bare invocation refuses loudly naming both, and naming both together refuses as opposite answers to one question. The declaration names what the DECLARER'S OWN SIDE does — --send-receive arms the declarer's own inbound to the counterparty, at answer-receipt for a knock and at redemption for a code, and nothing more. The RECEIVER verbs `knock approve` and `knock new-code` carry NO directionality flag at all: accepting IS the receiver's own-side act, so there is nothing further for them to declare, and the `approve --mutual` COUNTER-KNOCK IS REMOVED RATHER THAN RENAMED — a receiver who wants reverse reach knocks back, because the counter-ask is the knock verb's own job and a rider on approval duplicated it with a second grammar. VOCABULARY: send-only and send-receive are canonical; mutual and one-way are RETIRED from every user-visible surface — help text, refusals, stdout claims and the published guide alike — because 'mutual' misdescribes an act that only ever opens the declarer's side. MIGRATION IS A CLEAN BREAK WITH A SPEAKING REFUSAL: --mutual and --one-way are parse errors that NAME the flag which replaced them and the seat they were typed at, never clap's bare unexpected-argument, and there are no deprecation aliases (an alias keeps the retired mental model alive in scripts and agent habits). THE RENAME IS THE USER-VISIBLE SURFACE ONLY: store and wire field names are untouched, and PRE-AUTHORIZATIONS ARMED UNDER THE OLD FLAGS STAY HONORED — a record armed before this change still consumes through the answer-receipt path and still opens the reverse it was armed for. Gate: doc — the CONTEXT.md knock/knock-code directionality sentence and the mutual-whitelisting entry corrected by replacement so no entry states the retired seats, plus the published knocking guide; impl — the mandatory mutex at the bare form, `send` and `redeem`, the absence of any directionality argument at `approve` and `new-code`, the removal of the counter-knock path, and the retired-flag refusals; unit — a bare requesting invocation refuses naming both new flags, each flag ALONE is accepted, both together refuse, `approve` and `new-code` refuse the directionality flags outright, a retired flag's refusal NAMES its replacement at every seat that carried it, no user-visible string on the knock family carries the retired vocabulary, and — the load-bearing row — A PRE-AUTHORIZATION RECORD ARMED UNDER THE OLD FLAGS, DECODED FROM ITS SHIPPED BYTES RATHER THAN BUILT BY THE NEW CLI, STILL CONSUMES THROUGH THE RECEIPT PATH AND STILL OPENS ITS REVERSE, with the store and wire field names it names unchanged.
2026-09-06T13:09:02.1799579Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1799607Z 
2026-09-06T13:09:02.1799706Z ### REQ-KNOCK-INBOX-DAEMON-OWNED
2026-09-06T13:09:02.1803697Z - Title: The knock inbox is DAEMON-OWNED and durable independently of any agent's attach state: knocks land and persist whether or not the target's agent is running, attached, or resting, and an engine-room that is posture-offline loses nothing it was sent. This rules OUT the per-perch message spool as the substrate — that store is a property of a perch and dies with `endpoint purge`, which would make purging one endpoint destroy knocks addressed to another and would silently discard a pending knock at exactly the moment an operator is reorganizing endpoints. The store is therefore node-level and daemon-owned, and its durability is proven rather than asserted: purging the KNOCKER's perch leaves the knock answerable by the target, since the record's authority is the daemon's mint-time stamp and not the continued existence of the knocker's perch. Knocks are never auto-surfaced to the receiving agent — the inbox is queryable and the user or another agent must ask — with the sole exceptions being the three consented notifications. Gate: doc — the CONTEXT.md knock entry naming the inbox as daemon-owned and queryable-not-surfaced; impl — the node-level daemon-owned store; unit — a knock survives the target agent being absent and is answerable when it returns, purging the knocker's perch leaves the knock answerable, and no knock reaches an agent's event stream unbidden.
2026-09-06T13:09:02.1803817Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1803846Z 
2026-09-06T13:09:02.1803950Z ### REQ-HAZARD-WIRE-UNTAGGED-RECORD
2026-09-06T13:09:02.1809317Z - Title: A NEW WIRE RECORD MUST CARRY A `kind` TAG, because the untagged shape is already claimed. The stream dispatcher classifies a feed by sniffing its FIRST LINE, and the message family (`WanMessage`) is deliberately untagged: any first line carrying `target` plus `op_id` and no `kind` is read as a message feed. A new record type that omits the tag is therefore not merely unrecognized on an older daemon — it is MISCLASSIFIED as a message and DELIVERED INTO THE TARGET ENDPOINT'S SPOOL AS CHAT, on every N-1 node in the fleet, silently and without error. The failure is invisible to any same-version test, because both ends of a same-version pair agree; only a cross-version reading finds it. Discovered in W4 T10 (releases#53) while designing the knock record, which necessarily carries a `target` and would have hit exactly this arm. INVARIANT: every wire record minted from here on carries a `kind` tag that the dispatcher's tagged arm routes, so an older daemon's `Some(_)` catch-all resolves it to the Unknown family and DROPS it cleanly — no delivery, no wedge, no misread. The correct degrade for a request an old peer cannot serve is that it never arrives, never that it arrives as something else. PROOF OBLIGATION, both halves, or the pin goes vacuous: (a) this version routes the record to its own family AND is asserted NOT to route it to the message family; (b) the same bytes under a tag this version does not know resolve to Unknown; and (c) the untagged shape is asserted to genuinely BE claimed by the message arm — without (c) the test passes for the wrong reason the day the record's fields drift and it would no longer have collided anyway. Gate: doc — the KNOWN-HAZARDS entry stating the invariant and the misdelivery mechanism; impl — the tag on the record plus the dispatcher arm that routes it; unit — the three-part classifier pin above, run against the REAL classifier rather than a restatement of it.
2026-09-06T13:09:02.1809623Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1809651Z 
2026-09-06T13:09:02.1809746Z ### REQ-ACCESS-VERDICT-REASON
2026-09-06T13:09:02.1816060Z - Title: The endpoint access gate reports WHY a pass passed, not merely THAT it passed: the gate's verdict carries, on its allow arm, the reason the chain admitted the interaction — the same-node short-circuit, the reply exemption, an entry whose subject NAMED the peer (a proven sender endpoint or the origin node), an entry that matched only by SUBNET WILDCARD (an entry, but one that never named this peer), or a posture pass (an endpoint / node / captured-subnet mode, or the implicit-open bottom of the chain). The reason is computed AT THE GATE, in the one place that holds the whole chain, and travels with the decision. The alternative — a downstream consumer re-deriving it — means RE-OPENING the access store after the gate has closed it, which is both a second read of a value that may already have moved (the stale-snapshot class) and a second entry point into the security chain; the trust warning is a consumer of the decision, never a second decider. Refusals stay BARE: a denial's diagnostic tier is already printed where it is refused, and no enforcement caller does anything with a deny but drop it, so giving Deny a payload would force every refusal site to destructure a field it never reads. Enforcement sites therefore gate on a refusal PREDICATE, which is what keeps the ripple across them rename-shaped rather than a scatter of partial matches. Same-node and reply are carried as reasons in their own right precisely so that a consumer can never warn on them: same-node is inside the node's own trust unit, and a reply is traffic the receiving agent itself invited. Gate: impl — the pass-reason type, the payload on the allow arm, the tier-to-reason classification, and the predicate helpers, with every production enforcement site converted to the predicate; unit — one row per pass arm produced by the REAL gate rather than by calling the classifier directly, each row arranged so that only its own arm can produce the pass (the named-entry rows sit on a closed endpoint, the wildcard row's peer is a subnet member no rule names, and the same closed endpoint is asserted to refuse a non-member), plus the helper split: the refusal predicate is true for exactly the refusal, its complement holds for every pass reason, and no pass reason is readable from a denial.
2026-09-06T13:09:02.1816252Z - Required stages: impl, unit
2026-09-06T13:09:02.1816280Z 
2026-09-06T13:09:02.1816374Z ### REQ-FORK-MIND-COMPLETE
2026-09-06T13:09:02.1822080Z - Title: Forking an endpoint copies its tracked MIND TIER AS A WHOLE DIRECTORY AT TIP — every tracked file in the source's agent branch, enumerated from the tree itself — never a list of filenames the copy function happens to know. The enumerating shape is not a style preference: a fork that reads named files drops every mind file nobody remembered to add to the list, silently, one omission per file, and the loss is discoverable only by someone later noticing an absence. It had already happened in the field before this requirement existed: the live ROLE file (the endpoint's durable statement of purpose) was tracked on the same branch and was simply not copied, so every fork produced an endpoint whose role had vanished, and the next mind file to be added would have been lost exactly the same way. Copying the directory closes the past gap and the future ones in one structural move. EXACTLY ONE EXCLUSION, and it is a decision rather than an omission: surfaced-but-unresolved conflict artifacts stay with the mind that surfaced them, because a fork starts clean — a fork inheriting the source's unresolved pairs would make 'the fork starts clean' quietly false. Bytes are copied as bytes, not through a lossy UTF-8 round trip: the tier's file shapes are not the fork path's business. Sibling seams are unaffected by construction — a rename MOVES the whole worktree and so has never had this defect, and removal is explicit — so this requirement binds the fork path and is proved by a test whose load-bearing row is a file NO CONSTANT IN THE CRATE NAMES. Gate: impl — the tree-enumerated whole-directory copy with the conflict exclusion named as such; unit — a fork carries (a) a file no code path names, (b) a file nested a directory deep, so 'whole directory' means the tree rather than its top level, and (c) the live role file, the real gap this closes; while carrying NO conflict artifact, with the source keeping all of it, and the pre-existing copy-then-diverge behavior unchanged.
2026-09-06T13:09:02.1822304Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1822337Z 
2026-09-06T13:09:02.1822432Z ### REQ-MONIC-STORE
2026-09-06T13:09:02.1837104Z - Title: A monic — a REACTIONARY STRING: a trigger set plus a body revealed when something in the endpoint's session matches — is stored IN THAT AGENT'S OWN MIND TIER, ONE FILE PER RECORD, at `tracked/agents/<id>/monics/<monic-id>`. A MONIC IS NOT INHERENTLY ABOUT A PEER: classifying a peer is one use of ONE trigger kind, not the definition, and identity is the MONIC ID, which is not necessarily any of its triggers. Two properties are load-bearing and neither is a layout preference. FIRST, it is mind, so it replicates with the mind: same branch, same commits, same sync path, no second store to keep in step and no chance of an agent's instances disagreeing about what the agent decided because a side-store did not travel. SECOND, ONE FILE PER RECORD, because the mind's merge driver resolves PER FILE PATH and never merges contents — so the path granularity IS the conflict granularity. With every monic in one file, two instances of the same agent writing DIFFERENT monics concurrently would collide on that single path and surface a whole-file conflict — a conflict about nothing, between two facts that never disagreed, which the agent must then resolve by hand. Per-record files make those writes invisible to each other and confine a genuine conflict to the one record both instances actually wrote. Per-record is strictly finer-grained than per-peer, so this addressing IMPROVES conflict behaviour rather than trading it away. A SINGLE-FILE STORE IN THE MIND TIER IS REFUSED — sqlite or one json — for that same reason plus the sidecar problem: WAL/SHM files would materialise as untracked artifacts inside a git-replicated worktree. Lookup cost is a SEPARATE OBJECT from the record: if the delivery edge's scan is ever MEASURED to matter the answer is a node-local derived index outside the mind tier, rebuildable from the files, and NO INDEX IS MINTED UNTIL A MEASUREMENT ASKS FOR ONE. The record carries its own id as a field as well as in its filename, so a record recovered from a conflict artifact, a bundle or a copy still knows what it is and a filename/field disagreement is detectable rather than silently resolved. W5 RECORDS DO NEED A MIGRATION (operator-ruled 2026-08-03, superseding the previous never-migrated commitment, which covered ADDING A FIELD to a peer-keyed record and never covered a RE-KEY): a W5 record maps to id = the old peer-id, triggers = one sender trigger on that peer-id, body unchanged. The mapping is applied AT READ so every reader is correct immediately — including the store-free readers on the inbound message path, which cannot write anything back — and an in-place rewrite makes the stored bytes say what the record means; the rewrite is IDEMPOTENT, because every write here is a commit that replicates. A monic id is charset-validated as an endpoint id before it becomes a path component or a tracked filename — an unvalidated one is a traversal out of the mind — while a TRIGGER PATTERN is deliberately NOT so constrained, since it never touches the filesystem and must be able to carry arbitrary symbols. The store answers the trust warning's question as a NAMED PREDICATE — does this endpoint hold any monic whose SENDER trigger matches this peer, a scan of the monic directory rather than one path-addressed read — so the warning path never decides on its own what an absent-versus-unreadable record means; classification is monic-only, and an access-rule entry naming a peer is not a classification. Removal is idempotent and tracked — a withdrawn monic must not survive at the branch tip. Gate: impl — the monic-id-keyed record with its trigger set and origin provenance, the CRUD over the mind worktree with a checkpoint per mutation, the read-side legacy upgrade and the idempotent in-place migration, the classification predicate as a sender-trigger scan, and monic-id validation on every path-forming call; unit — the CRUD round trip (a rewrite REPLACES rather than accumulating, removal idempotent, an agent holding nothing lists nothing rather than erroring on an absent directory, monics are per-owner), a write and a removal are both visible at the mind's own branch tip, an illegal monic id is refused rather than written WHILE a trigger pattern full of metacharacters is accepted and matches literally, THE RE-KEY ROW — a monic's id is INDEPENDENT of the peers it classifies, proven by a record whose id is not the peer it classifies, one record classifying several peers, and several records classifying one peer, none of which is expressible under peer-keying — THE MIGRATION ROW — a planted W5 record reads correctly through every path before any rewrite, then rewrites in place to the current shape, idempotently, with a husk left as a husk rather than repaired into an invented record — and THE STORAGE-SHAPE ROW — two instances of the same agent writing DIFFERENT MONICS concurrently merge through the REAL sync path with ZERO conflicts and neither side clobbered, WITH a SAME-MONIC control proving the same path does surface a real disagreement (without the control the row would pass on a merge path that never conflicts about anything).
2026-09-06T13:09:02.1837480Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1837509Z 
2026-09-06T13:09:02.1837594Z ### REQ-MONIC-VERBS
2026-09-06T13:09:02.1855587Z - Title: An agent edits its own monics through `spt endpoint monic <list|add|update|remove> [--owner <id>]` and copies another endpoint's through `spt endpoint monic clone <monic-id>|--all --from <src> [--to <dst>] [--overwrite]`. ADD, UPDATE AND REMOVE REQUIRE `--target <monic-id>`; ADD AND UPDATE REQUIRE `--triggers <triggers-json>`; AND THE BODY ARRIVES ON STDIN, because a monic body is prose carrying newlines, quotes and shell metacharacters, which an argv-borne body hands to whatever shell is in the middle to mangle. ONE STDIN PAYLOAD MAY CARRY SEVERAL MONICS: the multi-record form takes whole records from stdin as a JSON array, each with its own id, triggers and body, and it is a DISTINCT INPUT SHAPE from the single-record form rather than a reinterpretation of it — the flags name the record and stdin is its body, so a payload that also had to carry ids would be two sources for one fact, and the surface refuses the two spellings together so neither is ever ambiguous. A multi-record write applies RECORD BY RECORD and does not roll back what already landed, because each record is an independent fact and a partial application that names exactly which ids landed is more recoverable than an all-or-nothing failure the operator must then diagnose; the exit code is non-zero if ANY record was refused, so a script cannot read a partial application as success. NO MONIC VERB IS EVER ELEVATION-GATED (ratified): a monic is the agent's own judgement, written by the agent, and an agent holds no OS privilege to prove — gating one would make an agent's own mind editable only by the human standing at the machine, which is the opposite of what the register is for. That invariant is a CHECKED PROPERTY, not a comment: the verbs' one decision point TAKES the process's real elevation and ignores it, so a gate added later in either direction fails a test rather than passing unnoticed as a filled-in absence. ADD AND UPDATE ARE SEPARATE VERBS because they refuse opposite states — add refuses to replace a monic that already exists, update refuses to invent one that does not — so a typo'd id or a re-run script cannot quietly change a record; the refusal names the other verb. THE LISTING SHOWS UNREADABLE RECORDS AS UNREADABLE. A record that is present and unparseable reads as never having matched at the delivery edge, which is correct there because it fails safe (it warns more, never less) — but a husk that also vanishes from the listing is a file the agent can neither act on nor discover, the erased-distinction class. So the list names it UNDER ITS OWN MONIC ID, marks it unreadable, and points at the verb that rewrites it. The listing also states a trigger kind that is RATIFIED BUT INERT as such, since a trigger that cannot fire today must not be presented as working. CLONE COPIES THROUGH THE FORK'S COPY SEAM (REQ-FORK-MIND-COMPLETE), not a second copy path, and copies the source's mind AT ITS TIP; a record the destination already holds under that id is KEPT and reported rather than silently replaced (--overwrite is the deliberate replacement), and every copied record is re-stamped as inherited so the destination can tell its own monics from the ones it was handed. A husk in the source travels as a husk — visible in the destination's listing, never dropped and never invented. A clone that copies nothing mints nothing: no branch, no worktree, no empty commit. Gate: impl — the five verbs over the monic store with owner defaulting to the session's own endpoint, the stdin body read, the trigger-set parse with an unparseable set REFUSED rather than defaulted to empty, the multi-record payload form, the elevation-taking decision function, the add/update preconditions, the unreadable-record row in the listing, and the clone routed through the shared copy seam; unit — the elevation table (every verb x record-present/absent yields the SAME outcome under elevated, not-elevated and undeterminable), the add/update split including the husk reading ratified 2026-08-04 (a husk is PRESENT for these verbs, so add refuses it with the unreadable diagnosis and update replaces it while announcing that it did — see REQ-MONIC-HUSK-PRESENT, which owns that polarity and its escape hatch), the clap surface (bare `monic` lists, --target and --triggers required without the multi-record flag and refused with it, clone takes exactly one selector), the two-part husk row (delivery edge says never-matched, list path names it unreadable under its id), and the clone rows (existing destination record kept and reported, --overwrite replaces, copies stamped inherited, source untouched, self-clone refused, nothing-to-copy mints no mind).
2026-09-06T13:09:02.1867770Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1867935Z 
2026-09-06T13:09:02.1868037Z ### REQ-MONIC-HUSK-PRESENT
2026-09-06T13:09:02.1876694Z - Title: A PRESENT-BUT-UNREADABLE monic record — a HUSK — COUNTS AS PRESENT when `add` and `update` classify what they found (ratified 2026-08-04, bag grill; this REVERSES the reading that shipped in W5 and was deliberately preserved through the releases#70 re-key). The question the two verbs ask is whether the monic id is already SPOKEN FOR, and a file filed under that id speaks for it whether or not anything can parse the bytes. So `add` REFUSES a husk, naming it as unreadable and pointing at `update` and `remove`, and `update` PROCEEDS over one, announcing the replacement out loud. The superseded polarity was not dishonest — both arms said what they saw — but it made `add`, the verb whose entire job is refusing to clobber what is already filed, the one that destroyed content, and `update`, the verb whose job IS replacement, the one that refused to perform it; it then instructed the caller to reach for the destructive verb in order to replace a record, teaching precisely the reflex the add/update split exists to prevent, in the ONE case where the operator cannot read the old content to judge what the replacement costs. THE ESCAPE HATCH IS VERIFIED, NOT BUILT: making `add` refuse a husk would strand a caller only if nothing else could clear a corrupt record, and `remove` decides on FILE EXISTENCE rather than readability, so it already withdraws a husk by construction — no `--force` flag is minted, and that shipped property is asserted by a test rather than trusted. THE POLARITY IS ONE FACT AND MUST BE SPELLED THE SAME EVERYWHERE IT IS ENCODED: the CLI's classify call, the store's own add/update primitive, and every printed line that names a verb for an unreadable record — a listing hint still pointing at `add` would be a surface instructing the caller to run the arm that now refuses. UNCHANGED, deliberately: the DELIVERY EDGE still reads a husk as never-classified (it fails safe — it warns more, never less), and the LISTING still shows a husk under its own id marked unreadable. Gate: doc — the CONTEXT.md mnemonics entry states the ratified polarity, the reason the previous reading was reversed, the verified escape hatch, and what stayed unchanged; impl — the classification input counts a husk as present at BOTH encoding sites (the CLI verb path and the store's write primitive, which classifies on the FILE question rather than on a successful parse), the `add` arm refuses with the unreadable diagnosis naming `update` and `remove`, the `update` arm proceeds and keeps the announced-replacement line, and the listing's advice names the verb that now replaces; unit — a husk REFUSES `add` as Exists AND SURVIVES THE REFUSAL UNREAD (the refusal is not a partial write, which is the whole point of refusing over content nobody can read), a husk is REPAIRED IN PLACE by `update` (the caller has no other route to repair), and `remove` withdraws a husk and stays idempotent over the cleared id, proving the escape hatch that made the `add` refusal safe to ship.
2026-09-06T13:09:02.1884408Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1884631Z 
2026-09-06T13:09:02.1884742Z ### REQ-KNOCK-MONIC-IMPART
2026-09-06T13:09:02.1892543Z - Title: A knock answer can impart a MONIC about whoever it admits: `spt knock approve <id> --monic <message>` writes the note at the approval, and `spt knock new-code --monic <message>` bakes one into a code that is imparted when the code is REDEEMED. The code path is deferred by necessity rather than by design — at mint there is nobody to write a classification about yet, and the redeemer's identity only becomes stamped-and-real at redemption. THE NOTE IS ABOUT A DAEMON-STAMPED ID, never a caller-supplied one: it is written about the same stamped knocker id an approval writes its whitelist entry for, so a note can no more be planted on an innocent peer than a grant can. A knocker with no endpoint id — a bare-terminal human, stamped as their node — CANNOT be classified: the note is refused loudly and the grant stands, because writing a classification under a node id would produce a record no delivery-edge lookup can ever match while reading back as though someone had been classified. AN EXISTING CLASSIFICATION IS KEPT. Both paths route through ONE shared impart primitive whose rule is that the owner's standing judgement outranks a judgement arriving with a grant — an approval must not silently rewrite what an agent already decided about a peer, and a note typed at code-mint hours earlier certainly must not — and the caller is told which happened so it prints the replace command instead of implying the note landed. An UNREADABLE record is not a classification, so imparting replaces a husk. THE NOTE RIDES THE GRANT AND NEVER CHANGES IT: it is imparted only AFTER the grant has committed, is skipped entirely on a refusal (a classification of a peer you did not admit), and a mind that cannot be opened costs a loud diagnostic rather than a retracted approval. Whose mind holds the judgement is RECORDED, not derived: a node-target code is minted by the engine room and a node has no mind, so the imprint carries its owner endpoint from the mint, and the redemption path never guesses. The code record's imprint field is ADDITIVE — a code minted before the field existed still reads, and a code with no note serializes without the key. Gate: impl — the `--monic` flag on approve (a NEW public flag; it existed only on new-code, where it was recorded and inert), the owner-carrying imprint on the code record, the shared impart primitive, and the impart calls at the approve and redeem sites after their commits; unit — imparting writes with knock provenance, KEEPS a standing judgement and reports what is held, replaces a husk, the imprint field reads with and without a note in the record bytes, and both answer surfaces parse the flag.
2026-09-06T13:09:02.1899507Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.1899678Z 
2026-09-06T13:09:02.1899774Z ### REQ-TRUST-WARNING
2026-09-06T13:09:02.1915215Z - Title: When a message reaches an endpoint because an access ENTRY admitted its sender, and the receiving agent holds NO MONIC about that sender, the delivery edge composes a system-authored TRUST WARNING and delivers it alongside the message. The entry is a routing decision — often made by a human at a knock, sometimes weeks earlier — and it says the peer may speak; it says nothing about whether the receiving agent has decided anything ABOUT them, and the warning is what closes that gap before the agent acts. THE CLASSIFICATION QUESTION IS MONIC-ONLY: an access entry naming the peer is NOT a classification, since the entry is the very thing that let them in, and reading it as evidence of judgement would silence the warning exactly when it is warranted. THREE RATIFIED NON-WARNING CASES, stated because a warning that fires on invited traffic teaches agents to ignore warnings, which costs it its only job: SAME-NODE arrivals are inside the node's own trust unit; a REPLY is correlated to the agent's own outbound, so it is traffic the agent itself invited; and a POSTURE-OPEN pass admits nobody in particular — no entry named this peer, the endpoint simply is not refusing anyone. A WILDCARD entry warns exactly as a named one does: it admitted a peer the agent never named at all. THE WARNING NEVER CHANGES DELIVERY. The gate already allowed the message; the warning is advisory text that rides alongside, is skipped for a duplicate (the replay check precedes it, so a retried message does not re-warn), and — when it cannot be delivered at all — costs a loud diagnostic naming the unwarned delivery rather than withholding a message the gate permitted. IT IS NEVER PART OF THE PEER'S BODY, because a warning inside the body is indistinguishable from one the sender wrote — precisely what a stranger would forge. THE CARRIER IS THE DELIVERED MESSAGE'S OWN ENVELOPE (REQ-TRUST-WARNING-ENVELOPE, which changed the carrier and left this rule untouched): the block rides as a `trust-warning` attribute composed by the RECEIVING node, so the caution and the message it is about reach the agent in ONE arrival, and the sender authors the text in neither design — an attribute is not the body, and it is composed exactly where a matched monic's is. Where the body is ALREADY A TYPED ENVELOPE it can carry no attribute, and there the warning keeps its own system-authored delivery under a RESERVED author id (not a legal endpoint id, so no peer can author under it), delivered FIRST over the same cascade the message takes, so the caution is read before the message it is about. THE DECISION CONSUMES THE GATE'S OWN VERDICT AND NOTHING ELSE: the pass reason IS the posture question (an entry matched means the chain never reached a mode tier), so there is no second read of the access store, whose snapshot would be taken after the decision it purports to explain. The receiver's own mind is read as a PLAIN FILE off the tracked root, never through the store's ensure-worktree path, because that path spawns git and this question is asked on the inbound message path. An unreadable monic is not a classification, so a husk makes the warning fire rather than suppressing it. TWO LIMITS, RULED (doyle 2026-07-31, at T6's acceptance): (1) a sender the daemon could not prove IS still warned about, and that warning is UNSUPPRESSIBLE by design rather than merely fail-safe — a peer nobody can name is more of a stranger, not less — since monic-only classification means no provable id yields no monic, so every unproven sender on an entry-admitted pass warns, which is honest — they ARE unproven — and self-heals as the fleet's daemons come to stamp their senders. THE TEXT DISCIPLINE IS THE LIMIT THAT KEEPS WARN-MORE FROM BECOMING WARN-NOISE, and is required, not stylistic: the block names the admitting RULE as the way out and NEVER prints a classify command that cannot be run, because an instruction the reader cannot carry out is what turns an unsuppressible warning into noise the agent learns to skip; (2) the override text (the elevation-gated verb, own requirement) replaces ONLY the advisory paragraph — RATIFIED at the same acceptance, on the grounding that override text is agent-behavior instruction and therefore a prompt-injection surface, so keeping the factual spine unforgeable BOUNDS that surface: a whole-block override would let one elevated write hide who is knocking, which no legitimate override needs — the line naming the sender and stating that no note is held, and the line saying how to classify them, are always core-composed, so an override can change what the agent is cautioned about but never hide who is knocking. Gate: impl — the pure warn-or-not decision over the verdict's pass reason and the monic predicate, the store-free tracked-root read, the composer with its reserved author, and the delivery-alongside at the WAN edge ahead of the message legs; unit — the full table (every pass reason x monic-held/not, with the three non-warning reasons asserted by name and the wildcard case asserted to warn), the composer naming the peer, the three cautions and the exact classify command, the override replacing the advisory while identity survives, the unnamed-sender text carrying no unrunnable command, and the tracked-root read answering identically to the store form while creating no worktree for an agent that has no mind.
2026-09-06T13:09:02.1929063Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1929227Z 
2026-09-06T13:09:02.1929325Z ### REQ-TRUST-WARNING-OVERRIDE
2026-09-06T13:09:02.1940471Z - Title: An operator can replace the ADVISORY PARAGRAPH of one endpoint's trust warning with their own text, through `spt endpoint trust-warning set <text> [--owner <id>]`, withdraw it with `reset`, and read what is in force with `show`. THE SCOPE IS PARTIAL BY RATIFICATION (doyle 2026-07-31, at T6's acceptance): custom text replaces the advice and NOTHING ELSE — the line naming who reached the endpoint and stating that no note is held about them, and the line saying how to classify them, are always core-composed. The grounding is that override text is agent-behavior instruction delivered under a RESERVED SYSTEM AUTHOR, which makes it a prompt-injection surface, and an unforgeable factual spine BOUNDS that surface: a whole-block override would let one elevated write hide WHO is knocking, which no legitimate override needs. THE VERB STATES THAT SCOPE IN ITS OWN OUTPUT — required, not stylistic: the operator writing agent-facing instruction is told at the moment they write it which lines they did not reach and why, rather than only in a doc they may never open. WRITES ARE ELEVATION-GATED AND READS ARE NOT, and this is THE ONE ELEVATION GATE IN THE MONIC/TRUST-WARNING FAMILY (REQ-MONIC-VERBS asserts the opposite invariant for the monic verbs, and this requirement must not be read as loosening it): writing is a human-at-the-machine act because an agent must not be able to rewrite the caution it is about to be handed, while showing what an endpoint is told leaks no authority and is exactly the visibility that makes a planted override discoverable — gating the read would protect nothing and hide the only thing worth auditing. Elevation that cannot be POSITIVELY CONFIRMED refuses like no elevation at all. THE TEXT IS STORED NODE-LOCALLY BESIDE THE ACCESS STORE, ONE FILE PER ENDPOINT, AND NEVER IN THE MIND TIER: a mind file replicates between every instance of an agent, so an override filed there would let a peer instance's mind push warning text onto this node — the injection surface the elevation gate exists to hold shut, re-opened by a sync. TWO WRITE-TIME REFUSALS: a blank override (withdrawal is its own verb, so a blank one is a shell mishap far more often than an intent to caution an agent about nothing) and one past a length bound (the block is read ahead of every admitted stranger's message with the classify line BELOW the advisory, so an unbounded advisory buries the line an override may not touch — the harm the whole-block override was refused for, reached by length instead of by scope). THE READ IS FAIL-SAFE AND STORE-FREE: the delivery edge reads the override off a plain path after it has already decided to warn, and an absent, unreadable, or blank record all read as NO override, falling back to the default advisory — what a damaged override costs is the operator's wording, never the caution. An override belongs to ONE endpoint: cautioning one agent never rewrites what another on the same node is told. Gate: impl — the store's per-endpoint node-local record with its validated write, fail-safe read and withdrawal, the pure verb decision keyed on whether the verb WRITES, the three CLI verbs with the scope notice in the set path, and the edge read wired into the composer's custom slot; unit — the gate table (every write refuses under not-elevated and undeterminable, show proceeds under every elevation, and gated-iff-it-writes asserted as the keying property), the refusal naming the endpoint and the elevated re-run, the stored override reaching the composer and a reset restoring the default, per-endpoint isolation, a blank/damaged record falling back to the default advisory, a traversal endpoint id refused on read and write alike, the blank and over-length write refusals, and the surface parsing with a bare `set` refused.
2026-09-06T13:09:02.1950427Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1950600Z 
2026-09-06T13:09:02.1950700Z ### REQ-TRUST-WARNING-CADENCE
2026-09-06T13:09:02.1967030Z - Title: The trust warning surfaces ONCE PER SESSION PER PEER, not once per message. THE CONDITION AND THE CADENCE ARE DIFFERENT QUESTIONS and this requirement exists because the first was ratified without the second: REQ-TRUST-WARNING settled WHEN a warning is owed (an entry admitted the sender and the receiver holds no monic) and never said HOW OFTEN, so the shipped edge composed one per admitted message. The dedup already named there is REPLAY dedup — a retried op-id is not re-delivered — which is a different question wearing similar words, and reading it as repetition coverage is what let the gap ship. A 665-byte block ahead of every message from one admitted stranger is a warning an agent learns to skip, which costs it the only job it has. THE DEDUP KEY IS (BOUND SESSION ID, PEER). The session id is the harness session BOUND TO THE RECEIVING PERCH, read from the perch record the delivery edge already holds a path to — one derivation serving both the live case and the offline-spool burst, so a hundred messages spooled while an agent was away surface ONE warning when it returns rather than a hundred. State lives in the node-local per-session scratch dir, one marker file per peer, which is the seam that already re-arms once-per-session state by construction: a `/clear` mints a new session id, hence a fresh dir, hence a re-armed warning. It is per-peer BY FILE and not a single seen-set file, deliberately — the seen-set's read-modify-write lost-update race was acceptable for keyword hints only because that writer is serialized per session, an assumption the WAN inbound path does not share. FOUR PROPERTIES, ALL FAILING TOWARD THE WARNING, stated because each is a place a later reader would 'fix' the caution away: (a) AN UNREADABLE MARKER AND AN ABSENT MARKER BOTH MEAN WARN, and this is CORRECT AND LOAD-BEARING rather than an oversight — it is the husk-reads-as-default shape, named here so a future fixer who reaches for a tidier read finds this sentence at the line they were about to change; a damaged dedup store costs a duplicate warning, never a silent one. (b) TWO MESSAGES FROM ONE PEER ARRIVING CONCURRENTLY MAY BOTH WARN — the check and the claim are not atomic across processes, and that is an ACCEPTED LIMIT, not a defect to close: the failure is a second warning, which is benign and direction-correct, and closing it would mean a lock on the inbound message path to save one duplicated advisory. (c) A MARKER WRITE FAILURE ALSO WARNS: the claim is best-effort, and an edge that could not record 'already warned' must behave as though it never warned. (d) TWO UNNAMED SENDERS ON ONE NODE SHARE ONE MARKER, because the origin node is the only identity there is to key an unprovable sender on — an ACCEPTED LIMIT that is the truth about what the daemon can name, not a collision to repair; a named peer and an unnamed one are never folded together, since the two arms carry distinct marker prefixes and neither class's marker can quiet the other. (e) NO BOUND SESSION RECORD MEANS NO KEY, AND NO KEY MEANS WARN EVERY MESSAGE — correct and rare, and the honest reading of a receiver whose session cannot be named. THE CLAIM IS MADE ONLY AFTER THE WARNING IS ACTUALLY DELIVERED, so the cadence can never eat the one warning a session gets: if every delivery leg fails (the loud undelivered diagnostic), nothing is claimed and the next message warns again — a marker on disk must never assert a caution that no agent ever read. THE CADENCE APPLIES UNIFORMLY, INCLUDING TO SENDERS THE DAEMON COULD NOT PROVE, and this does not loosen the unsuppressible-warning limit ratified in REQ-TRUST-WARNING: that limit is about CLASSIFICATION — no provable id yields no monic, so the monic path can never silence an unnamed peer — and a cadence is not a classification and silences nothing, since the caution still reaches the agent in every session and re-arms with each new one. Exempting unnamed senders would aim the exemption at the one class no monic can ever quiet, preserving the per-message flood exactly where it is unbounded. An unnamed peer's key is NODE-SCOPED because the origin node is the only identity there is to key on, so two unnamed senders on one node share one marker — that is what the daemon can honestly name, not a collision to repair. THE PEER COMPONENT OF THE PATH IS VALIDATED BEFORE IT IS JOINED, on the precedent the override store already sets, because the proven-sender id arrives off the wire: an id that fails validation yields no marker and therefore warns. THE OVERRIDE SEMANTICS ARE UNTOUCHED — cadence decides whether a block is composed at all, never what it says. Gate: impl — the per-peer marker claim over the per-session scratch dir with its validated peer component, the bound-session read at the delivery edge, and the claim-after-delivery wiring at the one surfacing site; unit — a second admitted message from the same peer in the same session composes no second warning, a new session warns again, per-peer isolation (a warning about A leaves B's first warning owed), a spooled burst of N messages from one peer yields ONE warning, an absent perch record warns every message, a marker write failure still warns, an unreadable marker still warns, an invalid peer component claims nothing and warns, two messages from UNNAMED senders on one node in one session yield one warning while a new session re-warns, a named peer and an unnamed sender on the same node neither quiet each other, and an undelivered warning (every delivery leg failed, the loud diagnostic printed) leaves the marker unclaimed so the next message warns again.
2026-09-06T13:09:02.1982777Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.1983011Z 
2026-09-06T13:09:02.1983134Z ### REQ-TRUST-WARNING-ENVELOPE
2026-09-06T13:09:02.2000363Z - Title: The trust warning RIDES THE DELIVERED MESSAGE'S OWN ENVELOPE as a `trust-warning` attribute composed by the RECEIVING node, so an agent reads the caution and the message it is about in ONE arrival rather than two. THE CARRIER CHANGES; THE RULE DOES NOT — when a warning is owed (REQ-TRUST-WARNING) and how often it surfaces (REQ-TRUST-WARNING-CADENCE) are untouched by this requirement, which settles only WHERE the composed block travels. THE GROUNDING IS THAT A SEPARATE DELIVERY IS A SEPARATE INJECTION: under an spt-hosted harness each delivery becomes its own context injection, so a caution delivered alongside costs the agent a second interruption for a block it must read with the message anyway. THE UNFORGEABILITY REASON IS PRESERVED, NOT TRADED, and this is the distinction the change turns on: the ratified rule forbids SPLICING THE WARNING INTO THE PEER'S BODY, because a warning inside the body is one a sender could have written. An envelope attribute is not the body — it is composed by the receiving node exactly as a matched monic's `mnemonics-json` is, so the sender authors it in neither design. THE ATTRIBUTE IS ADAPTER-VISIBLE BY DECISION, NOT BY ACCIDENT: the surface question was resolved before building and it was decidable rather than preferential, because the payload is text the AGENT MUST READ and any carrier stripped before the EVENT would ship a caution that never surfaces. It therefore enters the PUBLISHED envelope surface, which obliges the public envelope documentation to state it and a re-rendering adapter to surface it. FOR THIS ATTRIBUTE, BEING IGNORED IS THE FAILURE, which INVERTS the mnemonics-json precedent: an adapter safely ignoring an unknown attribute is exactly how a security caution goes missing, so 'additive and N-1-safe' is necessary and NOT sufficient here, and the doc must bind a custody pipeline that re-renders a delivery to carry the attribute through, on the precedent already set for carrying matched monics through intact. THE FAIL-SAFE IS NAMED, NOT DISCOVERED. A body that is ALREADY A TYPED ENVELOPE carries no attribute — it rides verbatim by construction, and splicing into a finished envelope would mean hand-rolling the grammar a second time. For exactly those deliveries the warning KEEPS its own system-authored delivery under the reserved author, unchanged. The second injection therefore survives precisely where no carrier exists and nowhere else, and the caution is never traded for the fix. THE INBOUND VALUE IS INERT BY CONSTRUCTION. A sender-supplied body that is itself a typed envelope rides verbatim through the wire, the spool and every renderer, so a peer can write this attribute into one and have it arrive wearing the receiver's own voice. Every point at which a sender-supplied body enters this node STRIPS the receiver-composed attributes BEFORE any is attached, so the value a recipient reads is the receiver's own by construction rather than by the sender's restraint. THE STRIP IS BY CLASS, NOT BY NAME: receiver-composed attributes are a named set, because two name-strips at one seam authored in two lanes is a drift pair, and inertizing the class costs exactly what inertizing one name costs while holding for the next receiver-composed attribute too. Attributes an envelope's own AUTHOR legitimately carries (`type`, `from`, a notify's id, an alarm's times) ride end-to-end intact — the strip removes named receiver-composed attributes from an otherwise untouched envelope, and it is not the wholesale re-compose that re-stamping an envelope's TYPE performs. A key that could not be re-emitted safely is DROPPED rather than written back unescaped. Gate: doc — the public envelope surface states the attribute, that the receiving node composes it, the MUST-surface obligation on a re-rendering adapter with ignored-is-failure as its reason, and the typed-envelope fail-safe; impl — the attribute constant and the receiver-composed class in the envelope grammar, the class strip at each sender-supplied ingress, the attach at the WAN edge through the envelope renderer so a matched monic still evaluates under the one rule, and the fail-safe branch that keeps the separate delivery when the body carries no attribute; unit — a forged inbound trust-warning attribute is inert on a delivered envelope, an admitted stranger's message carries the warning on its OWN envelope with no second delivery spooled, a typed-envelope body still draws the separate system-authored delivery, the strip preserves every other attribute and the body, and the cadence still claims only after the carrying message was actually delivered.
2026-09-06T13:09:02.2012711Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2012877Z 
2026-09-06T13:09:02.2012982Z ### REQ-MONIC-DELIVERY-TRIGGER
2026-09-06T13:09:02.2032292Z - Title: The monics a receiving agent holds that MATCH an arriving message RIDE THAT MESSAGE'S ENVELOPE as a `mnemonics-json` attribute carrying a JSON array of the matched records, so the agent reads its own standing judgement about the sender in the same breath as the message rather than having to go and look it up. THE MATCH IS EVALUATED AT THE ENVELOPE RENDERERS, NOT AT THE WAN EDGE (ruled 2026-07-31): a classification that fires only when the peer happens to be REMOTE is one the agent cannot rely on, and the trust warning's edge is the wrong home for it because that edge exists to consume the ACCESS GATE's verdict — the two are different concerns that happen to read the same register. THERE IS EXACTLY ONE EVALUATION PRIMITIVE and every delivery surface calls it: a match rule copied per surface is a classification that fires on one channel and not another, which is worse than none because the agent cannot tell which it is looking at. The renderers therefore take the RECIPIENT's id as well as the structural sender/body pair, and NO OWNER-LESS RENDERER REMAINS — a renderer that did not need the recipient is the shape a future delivery surface reaches for by habit, arriving attr-less with nothing failing. THE ATTR IS PRESENT IF AND ONLY IF SOMETHING MATCHED: an empty array would say 'evaluated, no match' where absence says 'not evaluated', a distinction no consumer has a use for and a second shape every consumer would have to handle. THE TRIGGER VOCABULARY IS RATIFIED (operator ruling 2026-08-03, superseding W5's sender-match-only limit) AND IT BINDS BOTH CONSUMERS — this edge and the now-signal MONICS surface — precisely so that the second consumer adds a CONSUMER and never a record migration. Triggers are a JSON array of matchers over five kinds: `sender` (identity match on the PROVEN sender id, which is the trust warning's classification question and the only kind that answers it), `content` (the incoming message body), `json` (a custom payload), `user_input` and `agent_output`. MATCHING COPIES THE KEYWORD-HINTS RULE VERBATIM — a literal CASE-INSENSITIVE SUBSTRING by default, a REGEX when `regex` is set, and an INVALID REGEX NEVER MATCHES (a bad pattern silences its own trigger and never panics) — and it copies it BY CALLING IT: the rule lives in one place with two consumers, because a rule copied verbatim into a second place is two rules that merely agree today. Literal-by-default is what lets a trigger carry arbitrary symbols with no escaping burden. WHICH KINDS HAVE A LIVE CONSUMER IS A SEPARATE QUESTION FROM WHICH ARE RATIFIED: `sender`, `content` and `json` evaluate at this edge, which exists; `user_input` and `agent_output` are RATIFIED BUT INERT until the now-signal / IO-parser funnel is built, and a record carrying one is not malformed — it is waiting for its consumer. Reading 'ratified' as 'has a live consumer' is the error this clause exists to prevent, and any surface that lists a trigger must say which it is looking at. THE TWO EDGE QUESTIONS ARE DELIBERATELY ASYMMETRIC AFTER THE RE-KEY: a content-triggered monic rides a message it matched WITHOUT classifying its sender, because classification is a `sender` question — so the delivery attr can be present where the trust warning still treats the peer as a stranger. The direction that must hold is the one that protects the agent: anything that classifies the sender ALSO matches the delivery, so an agent is never warned about a stranger whose monic it was handed in the same envelope. The ARRAY shape is not premature generality but the ratified envelope contract, and it is what lets a trigger kind matching several records ride with no envelope change. THE READ IS STORE-FREE AND PURE-PATH, off the tracked root, for the reason the trust warning's is: it happens on the inbound message path, where the store's ensure-worktree spawns git per message. A RECIPIENT WITH NO MIND — a shell link, an agent that has classified nobody — READS NOTHING AND GETS NO ATTR, and that is the primitive telling the truth rather than a case to special-case; it costs one sentence in the module doc, not a branch. An UNREADABLE record rides nothing, the same fail-safe reading the trust warning takes, and the two consumers are asserted to agree over every state a record can be in — an agent must never be warned about a stranger whose monic it was handed in the same envelope. The attr KEY is a compile-time constant and its VALUE is attr-escaped by the one composer, so monic text carrying quotes or angle brackets cannot forge an attribute or terminate the envelope. STATED LIMIT, NAMING ITS ONE PEER-TRAFFIC CASE: a body that is ALREADY a typed envelope passes through verbatim and carries no attr — splicing an attribute into a finished envelope means re-hand-rolling the grammar this codebase consolidated into one composer, and typed envelopes are otherwise machinery (notify, file-drop, echo-commune) that is not peer chatter. THE ONE CASE WHERE IT BITES PEER TRAFFIC IS `restamp_wan_user_msg` IN THE WAN EDGE, named here exactly so a later wave can lift it without re-deriving which path is meant: a FORGED `user-msg` — one whose sender the daemon could not prove — is re-stamped into a typed plain-`msg` envelope, which then rides as a typed body and gets no attr. What is lost there is the MONIC RIDE, never the caution: the trust warning is composed at that same edge from the gate's verdict and is untouched by this leg, and an unprovable sender is warned about unsuppressibly in any case. Lifting the limit means re-composing the restamp through the owner-aware renderer, which requires threading the RECIPIENT into `restamp_*` — real work on adversarial-path code, deliberately not taken in this wave, and to be requested through the board rather than bled into a neighbouring change. A SENDER-SUPPLIED TYPED ENVELOPE CANNOT CLAIM THE RECEIVER'S JUDGEMENT: every inbound `mnemonics-json` attribute is stripped at `receive_wan` before delivery, while the peer's body remains unchanged; the forged value is inert. Gate: impl — the one match primitive over the tracked root, the msg composer's extra-attribute form, the owner-aware renderers on both delivery edges, and every production render site converted to hand over its recipient; unit — a sender-matched monic rides as a JSON array carrying the record verbatim with `from` still first and BOTH edges composing identically, an unclassified sender and a mindless recipient get no attr at all, a husk rides nothing AND reads identically to the warning predicate over every state, a typed body carries no attr even when a monic matches, monic text full of envelope metacharacters cannot break out of the attr, and the attr survives the listener's chunk/reassemble round trip.
2026-09-06T13:09:02.2049492Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2049648Z 
2026-09-06T13:09:02.2049753Z ### REQ-BROKER-STOP-ENDPOINT-DENY
2026-09-06T13:09:02.2058095Z - Title: A broker-stopping command invoked from a context that resolves to an spt ENDPOINT is a FULL NO-OP that refuses, under every flag combination. Two commands are broker-stopping: `spt daemon stop` (INCLUDING `--force`) and `spt update apply --finish`. THE GROUNDING IS THAT THE CALLER CANNOT SURVIVE ITS OWN COMMAND: stopping the broker kills the hosted PTY of every spt-hosted endpoint on the node INCLUDING THE INVOKER'S OWN, so the agent's next chained command dies with the session that would have reported the outcome — the failure is silent from the only side that could see it, which is why this is enforced at the tool layer and not written as advice an agent is asked to follow. THE HUMAN CONTRACT IS UNTOUCHED: the pre-existing live-session refusal keeps its exact shipped behavior and `--force` remains a human's override of it. The endpoint deny is deliberately NOT overridable by any flag, because the caller who would reach for `--force` here is precisely the caller who cannot live through the result; an override would be a footgun with a safety catch that only the wrong hand can reach. THE RESOLUTION SEAM IS REUSED, NEVER RE-INVENTED: the same agent-ground resolution the engine-room ceremony refusal already uses answers this question, so 'what counts as an agent' has ONE definition on this node and a change to it can never make two refusals disagree. REFUSAL PRECEDES EVERY SIDE EFFECT — before the live-session guard, before the stop-inhibit is minted, before any teardown — so a refused call leaves nothing behind to clean up. ONE GUARD SITE PER COMMAND FAMILY, and for the finish command that site is INSIDE the apply function's finish arm, which is what BOTH the explicit `update apply --finish` verb AND the composite update's lethal-last Finish leg route through: guarding the verb alone would leave the composite lethal, and guarding both separately would be two rules free to drift. The composite already treats a failed finish leg as non-aborting, so the refusal degrades into the existing contract instead of inventing one. THE NON-FINISH `apply` ARM STAYS UNGUARDED and that is a decision, not an omission: it hands off to the running broker in place, the broker survives, the invoker's PTY does not die, and guarding it would deny agents a leg that is safe for them today. Gate: impl — the pure deny decision over the reused agent-ground resolution, wired at both sites ahead of every side effect, with the single-site rationale stated at the finish guard so no one later adds a second one at the verb; unit — an endpoint context is refused with force=true and force=false alike, a human context is unaffected in both, the finish arm refuses while the non-finish arm proceeds, and the decision is pure over its ground input; int — an e2e proves an endpoint-context caller cannot stop the daemon by ANY flag combination and that the daemon is still running afterwards.
2026-09-06T13:09:02.2065617Z - Required stages: impl, unit, int
2026-09-06T13:09:02.2065783Z 
2026-09-06T13:09:02.2065889Z ### REQ-BROKER-STOP-DENY-NAMES-BLAST
2026-09-06T13:09:02.2072499Z - Title: The endpoint-deny refusal TELLS THE CALLER WHAT IT COST THEM AND HOW TO PROCEED, carrying TWO required elements rather than a bare denial. FIRST, THE BLAST RADIUS BY NAME: the refusal states that the operation would kill the running sessions of all spt-hosted endpoints and LISTS THE RESIDENT SESSIONS, reusing the composition the live-session refusal already prints — a refusal that says only 'denied' teaches nothing, while one that names the eight endpoints about to die explains itself. SECOND, THE GROUND AND ITS CURE: the refusal names WHICH ground identified the caller as an endpoint, and the ONE action that clears THAT ground. THE PRINTED ACTION MUST BE TRUE FOR THE GROUND THAT ACTUALLY FIRED — an environment-variable ground prints unsetting that named variable, an ancestry ground prints running from a shell not descended from an agent session — because an instruction that cannot clear the condition it is offered for is the same defect as a warning that prints a command the reader cannot run, arriving from the opposite direction. THE REASON IT IS SAFE TO PRINT A CLEARING ACTION AT ALL is that clearing the environment does NOT clear the ground: an agent that unsets its identity variables is STILL caught by the ancestry leg, so the printed line helps a HUMAN whose shell merely inherited the variable while doing nothing for an agent trying to escape. That property is load-bearing and therefore PINNED BY ITS OWN UNIT — environment variables cleared plus an ancestry ground still refuses. Without that pin the printed unset line IS an escape hatch and this requirement inverts from a guard into a bypass manual, which is exactly the failure mode a prohibition written as text rather than enforced at the tool layer always reaches. Gate: impl — the two-element refusal composition with the per-ground clearing action selected from a TYPED ground rather than by matching the ground's prose, so the two can never disagree about which leg fired; unit — the refusal carries BOTH elements (the resident list AND the ground with its clearing action), each ground kind yields the action that actually clears it, and the escape-hatch pin: env cleared + ancestry ground still refuses.
2026-09-06T13:09:02.2078358Z - Required stages: impl, unit
2026-09-06T13:09:02.2078509Z 
2026-09-06T13:09:02.2078610Z ### REQ-INBOUND-RECORD-WIRE-FIDELITY
2026-09-06T13:09:02.2088161Z - Title: THE INBOUND RECORD ROUND-TRIPS EVERY VERDICT VARIANT, AND THE PIN IS EXHAUSTIVE BY CONSTRUCTION RATHER THAN BY A LIST SOMEONE REMEMBERED TO EXTEND. `InboundRecord` carries `binder_pid` and `binder_path` and FLATTENS `InboundVerdict` beside them; the `PathMismatch` variant carried a `binder_path` of its OWN, so serializing it emitted a LITERAL DUPLICATE KEY, `read_current` failed the parse, and the reader fell closed to `Unknown` (releases#172; the duplicate confirmed in the on-disk record on HFENDULEAM 2026-08-06). The one verdict class the record exists to surface was STRUCTURALLY UNRENDERABLE: the file plainly said `path_mismatch` while `spt subnet status` said unknown, and the operator learned of the real mismatch only from a manual probe. IT STACKED WITH A SECOND DEFECT — the firewall rule genuinely WAS wrong — which is the shape that makes a self-erasing diagnostic expensive rather than merely untidy: the instrument went quiet in exactly the condition it was built to announce. THE COLLIDING FIELD IS RENAMED ON THE WIRE (`running_path`), NOT UN-FLATTENED: un-flattening would move every OTHER variant's shape too, for a defect that lives in one variant, and the flattened form is what every existing record on every node already carries. The variant keeps a path of its own rather than borrowing the envelope's, because `InboundVerdict::warning()` renders from the verdict ALONE and has no envelope in hand — a renderer that reached for one would be a second source for a field the record already owns. MIGRATION IS NOTHING, AND THAT IS DERIVED RATHER THAN HOPED: an old duplicate-key record already reads `Unknown`, which is the safe reading, and the next binder write replaces the file wholesale. The reader stays FAIL-CLOSED — an unparseable record is still `Unknown`, never a guess. WHY THE COVERAGE THAT EXISTED WAS GREEN THROUGHOUT: `REQ-INBOUND-VERDICT-RECORD-BINDER-PINNED`'s unit sentence says a live binder round-trips its verdict, and it was satisfied — by round-tripping the VARIANT, never the variant THROUGH THE ENVELOPE. The collision cannot exist until the two are composed, so every test that touched one half passed. The pin this requirement adds is therefore not another round-trip; it is a WILDCARD-FREE MATCH over the enum that stops the crate compiling until a newly added variant is placed in the table, plus one specimen per variant with EVERY field a distinct value (identical fillers would hide a field that round-trips to a sibling's value). Gate: doc — the published status-JSON field table and example in the inbound-reachability page, carrying the rename and the reason the old name was never emitted; impl — the renamed variant field and its producers and renderers moved with it, one name on the wire and one in the code; unit — every variant written through `write_to` and read back through `read_current_from` under a live-binder oracle, the variant tags asserted against WRITTEN-OUT LITERALS rather than counted, the failing set reported with the on-disk body so a red names which variant and what the record actually held, and the exhaustiveness carried by a match the compiler enforces.
2026-09-06T13:09:02.2096438Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2096592Z 
2026-09-06T13:09:02.2096696Z ### REQ-INBOUND-VERDICT-SURFACED
2026-09-06T13:09:02.2107805Z - Title: The inbound-reachability verdict is carried on the MACHINE surface — `spt subnet status --json` — in EVERY state, present always and omitted never. THE SCOPE IS `--json` ONLY, and the HUMAN/MACHINE ASYMMETRY IS THE DESIGN RATHER THAN A GAP LEFT OPEN: the human surfaces (the coming-online banner, `spt subnet status`, its `--nodes` view) stay WARNING-ONLY by ratification — they render `InboundVerdict::warning()`, which is `None` for `Ok` AND for `Unknown`, so a verified-healthy node and an unsayable one both say nothing there, and that silence is deliberate. A reading view earns its lines: M8 decision 12 narrowed HINT_FOOTER out of `subnet status` on exactly that argument, and a line that says 'inbound is fine' on every healthy run is the noise that teaches an operator to stop reading the one run where it is not. A MACHINE READER HAS THE OPPOSITE NEED and it is not a matter of taste: a consumer that must distinguish verified-ok from cannot-say from blocked cannot do it by ABSENCE, because absence already means 'this reader is old' or 'this field was skipped', so a field that appears only when something is wrong makes the healthy state and the unimplemented state the same bytes. THREE CONSTRAINTS, ALL REQUIRED AT MINT AND ALL LOAD-BEARING. (a) THE FIELD RIDES BESIDE `peer_health_verdict` IN THE SAME PAYLOAD AND IS POPULATED IN EVERY ARM INCLUDING `Ok` AND `Unknown`, NEVER OMITTED — no `Option`, no `skip_serializing_if`. The `Ok` arm is the row a warning-only mindset forgets, and it is the row the whole requirement exists for. THE PAYLOAD HAS TWO EMIT SITES, not one — the membership view and the `--nodes` view both construct it — and BOTH carry the field, because a field that appears and disappears with an unrelated flag is a field no consumer can depend on. (b) THE STATES MIRROR THE STORE'S VERDICT VOCABULARY 1:1 AND NO NEW VOCABULARY IS INVENTED AT THE RENDER SITE. This is satisfied STRUCTURALLY rather than by a mapping function: `InboundVerdict` already derives `Serialize` with `serde(tag = "verdict", rename_all = "snake_case")`, so the store already publishes the wire spelling of its own states and the render site EMBEDS THE VERDICT rather than translating it. A token table in the CLI would be a second vocabulary that agrees with the first only until someone adds a variant — the drift class the surface table's single-source discipline exists to prevent — and it would also drop the `fix` command each non-`Ok` variant carries, which the store builds ONCE in the process that knows the real path and port and which a machine consumer would otherwise have to re-derive wrongly. (c) THE UNITS ASSERT, BY NAME: each state serializes to its documented value; the field is PRESENT IN THE `Ok` ARM; and the HUMAN PROSE IS UNCHANGED IN `Ok` AND `Unknown` — that last one is a characterization test guarding the docs-only half of this ruling, so a later change that starts printing an all-clear line on the reading view fails here rather than in the field. STATED CONSEQUENCE OF 'NEVER OMITTED', named so it is not later read as a leak: the human path computes its hint only when the daemon is running, while the `--json` arms read the verdict unconditionally. With the daemon down the binder pin resolves no live process and the read returns `Unknown` on its own, so daemon-down serializes as `unknown` with no special-casing and no vocabulary outside (b) — the machine surface says 'cannot say' in a state where the human surface deliberately says nothing at all. Gate: impl — the verdict field on the `subnet status --json` payload, populated unconditionally at BOTH emit sites; unit — every state serializes to its store-defined spelling, the field is present in the `Ok` arm and in the daemon-down `Unknown` arm, both emit sites agree, and the human render stays byte-identical under `Ok` and `Unknown`.
2026-09-06T13:09:02.2117770Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2117950Z 
2026-09-06T13:09:02.2118079Z ### REQ-KNOCK-PRESCRIBED-REMEDY-HONORED
2026-09-06T13:09:02.2126337Z - Title: A REMEDY THE CLI PRESCRIBES IS ONE THE CLI HONORS. `spt knock list` prints, beside every waiting knock, the command that answers it; running exactly that command from exactly that seat must not then be refused by the approve path. The defect this closes was observed verbatim in the field (releases#66, flynn -> Librarian during the v0.49.0 verify): the listing prescribed `spt knock approve <id> --approve-requested`, and the same binary answered that invocation with `not approved — <surface> stays pending, which only the engine room can answer`. PRESCRIBE-THEN-REFUSE IS THE WORST OF THE THREE AVAILABLE BEHAVIOURS, and the reason is not tidiness: a printed command is read as authoritative — more so than prose, because it is copy-pasteable — so a wrong one spends the operator's trust in the surface that exists to be trusted, and it does so at the moment they are deciding whether to widen access. Refusing with an explanation would be honest; prescribing the honored form would be helpful; prescribing an unhonored form is neither, and it is indistinguishable from the tool being broken. THE FIX IS A SHARED PREDICATE, NOT A SECOND LITERAL. The listing must compose its remedy from THE SAME decision the approve path evaluates, so the two cannot drift apart again — the single-source discipline the surface table already carries for attributability, applied to the prescription. A remedy composed by re-typing what the approve path is believed to do is the same defect one refactor later. WHERE NO INVOCATION FROM THE LISTING SEAT WOULD SUCCEED, THE LISTING SAYS SO AND NAMES THE SEAT THAT CAN ANSWER, rather than printing a command that seat cannot run: the honest arm of a prescription is sometimes 'not by you', and that is information the operator can act on. THE PRESCRIPTION MUST NEVER RECOMMEND A WIDENING THE GRANT DOES NOT REQUIRE. Stated as its own constraint because it is the failure mode a naive reading of the field report produces: telling an operator to add the whole-machine acknowledgment for a grant that binds a single proven sender would trade a confusing refusal for a silent over-grant, which is strictly worse — a refusal costs a round trip, an unnecessary widening costs the access boundary. Gate: impl — the shared answerability predicate and the listing composing its remedy line from it; unit — a knock the listing seat can fully answer prescribes an invocation that the approve path accepts, a knock it cannot fully answer prescribes the form that WOULD succeed or names the seat that can answer instead of printing an unhonored command, no prescription recommends an acknowledgment the grant does not require, and the listing's prescription and the approve path's decision are asserted to be the same predicate rather than two agreeing literals.
2026-09-06T13:09:02.2133739Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2133900Z 
2026-09-06T13:09:02.2133997Z ### REQ-KNOCK-CODE-SEALED
2026-09-06T13:09:02.2140411Z - Title: A KNOCK CODE CARRIES ITS OWN ROUTE, SEALED TO THE MINTING NODE'S SUBNET (ADR-0054, ratified 2026-08-01). The string is `sptkc_` + unpadded lowercase base32 of one envelope; the prefix is the FORMAT VERSION MARKER (a future format mints a new prefix, never a flag day) and the standing agent-recognition hook. Payload = the minting node's SHORT key (4 B) + the code secret (10 B): 80 bits is ample under the receiver-enforced redemption rate limit and the 24 h TTL, and the length is bought with payload economy because NO INVERTIBLE TRANSFORM EMITS FEWER BITS THAN IT CARRIES -- 'shrink by adding an encryption layer' is refused on the record so it cannot resurface looking clever. SEAL, as ruled: keys HKDF-SHA256 from the subnet seed under DISTINCT domain-separated labels for encryption and authentication (a tag key and a cipher key must never be the same key); the tag is truncate-8 of HMAC-SHA256 over the PLAINTEXT and that tag IS the SIV nonce, padded to the cipher's nonce width, so the envelope is tag||ciphertext at 22 B and ~36 base32 chars -- SHORTER than the shipped 32-hex form. Determinism is safe ONLY because the 10 B secret is fresh per code; a reused secret would repeat an envelope, and that is the property this construction rests on. Verification is CONSTANT-TIME. THE REDEEMER TRY-DECRYPTS with every subnet key it holds, CURRENT AND PREVIOUS SEED, so a code inherits the subnet's own one-deep rotation grace (Mesh-D7) rather than a private key schedule -- and EVERY failure is INDISTINGUISHABLE from every other: a wrong subnet, a tampered byte, a bad length and an unknown code all return the same nothing, because a decode that reported WHY would be the oracle the redeem refusals already refuse to be. A pasted code reveals nothing to a non-member -- not the node, not the subnet, not which subnet -- and the decode-set is exactly the population that could dial the minter anyway, so no capability is lost. LEGACY BARE-HEX CODES STAY LOCAL-REDEEMABLE, untouched. Gate: doc -- the CONTEXT.md sealed-code entry naming the format, the payload economy and the rotation grace; impl -- the codec, its two derived keys and the try-decrypt; unit -- a round trip through a real seed, a code sealed to one subnet refusing under another, the previous seed still opening a code the current one minted before rotation, a single flipped byte anywhere in the envelope refusing, every refusal shape returning the identical nothing, and a legacy bare-hex code still reading as itself.
2026-09-06T13:09:02.2146867Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2147027Z 
2026-09-06T13:09:02.2147138Z ### REQ-KNOCK-CODE-MULTI-ENVELOPE
2026-09-06T13:09:02.2158027Z - Title: A CODE MINTED ON A MULTI-SUBNET NODE OPENS FOR EVERY MEMBERSHIP THAT NODE HOLDS, AND THE MINT SAYS SO. The ratified design always had it (ADR-0054 sketch line 17, `--subnet` selecting only when the minter belongs to several); the W4 build DEFERRED the slice and marked it UNBUILT in amendment 3, and the shipped surface then taught the deferral as the design -- `--subnet` took exactly ONE value and several memberships with no `--subnet` REFUSED with `KNOCK_WHICH_SUBNET`, whose copy states 'a sealed code opens only for members of ONE subnet' as though it were a rule rather than a gap. The operator's restatement (2026-08-04) governs and supersedes amendment 3: `--subnet` TAKES A LIST, and OMITTED means seal for ALL memberships. ONE SECRET, N KEYS -- NEVER N SECRETS: the store record keys on the secret, so a second draw would mint a code whose envelope opens to a secret no record holds, and this is also what keeps the SIV construction safe, since the envelopes differ by KEY and never by a repeated (key, plaintext) pair. The envelopes ride ONE string, concatenated at a fixed 22 B width so the count is DERIVED FROM THE LENGTH rather than declared in a header -- nothing in the string says how many there are, for the same reason nothing in it says which subnets they are for. THE DISCLOSURE DELTA IS ACCEPTED AND STATED RATHER THAN PADDED AWAY (doyle, 2026-08-18): a concatenated code reveals HOW MANY memberships the minter holds, never WHICH, and padding to a fixed envelope count would spend the length budget the whole ADR defends in order to hide a fact strictly weaker than the ones the format actually hides. THE N-1 ARM IS REAL AND IS ANSWERED AT THE MINT, NOT BY A VERSION FLOOR: an older binary refuses a multi-envelope string on its length check and reports it through the anti-oracle silence every other failure uses, so no redeemer can ever diagnose it -- and a new prefix would not help, since an unknown prefix routes to the legacy path and misses just as silently. The mint is therefore the only place the fact can be told, and it tells it: sealing N>1 envelopes PRINTS the consequence, naming that the code redeems only at or past the version that ships this. No fleet version floor is claimed, because a floor claimed before the feature ships is a floor about an unshipped thing. TWO OPENINGS ARE NOW POSSIBLE AND THE WINNER IS PINNED, NOT INCIDENTAL: a redeemer holding two of the minter's subnets opens two envelopes, and the winner selects the registry that resolves the short node key -- so the loop's exit condition is ROUTED, NOT OPENED (try every opening, prefer the first that resolves a route), with ties broken by SUBNET-NAME LEXICOGRAPHIC ORDER so the choice is a stated property rather than an artifact of store iteration. This cannot reach a wrong destination by construction: the short key is the MINTER's, so every opening names the same physical node, and preferring a routed one can only avoid a thin registry's NoRoute. Gate: doc -- the CONTEXT.md sealed-code entry gaining the multi-envelope mint UX and the count-disclosure qualifier; impl -- `seal_to_all` over N seeds sharing one secret, the length-derived envelope split, `open_all_among` returning EVERY opening with its label, the redeem route's prefer-routed selection with the lexicographic tiebreak, the mint's subnet-list choice replacing the single-choice refusal, and the N>1 mint print; unit -- a two-membership code opening under BOTH seeds and yielding the SAME secret and node (the property the one-draw rule exists for), the single-envelope form byte-unchanged and still exactly 36 base32 chars beside a multi-envelope sibling pinning the N>1 length as a LITERAL (71 for N=2, because the envelopes are base32-encoded as ONE blob rather than encoded separately and joined -- twice-36 is the wrong figure and a derived expectation gets it wrong, which is why the pin is written out), a non-member opening NOTHING from a multi-envelope code (the anti-cross-open pair kept as the negative control), prefer-routed selecting the subnet that RESOLVES when the lexicographically-first one does not, the lexicographic tiebreak deciding when BOTH route, and the previous-seed rotation grace still opening one envelope of a multi-envelope code.
2026-09-06T13:09:02.2169068Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2169475Z 
2026-09-06T13:09:02.2169655Z ### REQ-KNOCK-CODE-ROUTE
2026-09-06T13:09:02.2175817Z - Title: THE REDEEMER RESOLVES THE MINTING NODE FROM THE CODE, NEVER FROM DISCOVERY. The sealed payload's 4 B short key is matched against the node-tier rows the redeemer's own subnet registry already replicates -- node-level labels and the authoring nodes of instance rows -- because a code exists precisely to reach an endpoint that discovery does NOT surface. Routing through endpoint DISCOVER would rebuild the hidden co-requisite the fork-surface rig finding already paid for (releases#76 grounds): any cross-node verb that resolves its target through registry replication inherits a DISCOVER dependency it never declared. THE RESOLUTION SET IS THE UNION of both node-tier carriers rather than the label map alone -- a member that never named itself holds no label row, and that member is exactly the undiscoverable minter this feature exists to reach, so a label-only lookup would fail for the case the feature was built for. AN AMBIGUOUS PREFIX REFUSES AND NEVER PICKS: 4 B is 32 bits, collision is negligible and not zero, and guessing dials a stranger with a live code -- the resolution policy's own refuse-and-qualify discipline, applied where the cost of a wrong guess is a capability handed to the wrong machine. Resolution runs INSIDE the one subnet whose key opened the envelope, so the ambiguity surface is that subnet's node set rather than every node known. Gate: doc -- the networking entry stating what the route resolves against and what it must not widen; impl -- the union lookup and the ambiguity refusal; unit -- a short key resolving to its full node key through each carrier INDEPENDENTLY (a label-only row and an instance-only row, so neither leg can be the only one working), two nodes sharing a prefix refusing rather than resolving to either, and an unresolvable short key reading as its own outcome rather than as a refusal by the far end.
2026-09-06T13:09:02.2181998Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2182173Z 
2026-09-06T13:09:02.2182275Z ### REQ-KNOCK-REDEEM-WIRE
2026-09-06T13:09:02.2192606Z - Title: REDEMPTION TRAVELS AS ITS OWN KIND-TAGGED WIRE FAMILY, never a field on `KnockRecord`. Extending the knock record was refused on the record and the reason is the decisive one: UNKNOWN FIELDS DECODE FINE, so an older minter would ACCEPT a redemption as an ordinary knock -- pending in its inbox, no auto-approve, the code unconsumed, a knock-shaped ack returning -- and MISREAD IS WORSE THAN DROPPED. A new kind means an N-1 daemon resolves it to the unknown family and DROPS IT CLEAN, which is the honest failure. THE REDEEMER'S IDENTITY IS DAEMON-STAMPED at the proven-sender tier and never caller-supplied, the same seam the knocker stamp already rests on. THE OUTCOME IS A TRIPLE AND THE THIRD MEMBER IS LOAD-BEARING: REDEEMED (the granted surfaces, the one-directional sentence and the reverse command), REDEEM_REFUSED (ONE anti-oracle message covering unknown, expired, consumed-by-ANOTHER-EXCHANGE AND rate-limited alike -- WHICH budget or which reason a caller exhausted is itself the oracle), and REDEEM_UNCONFIRMED (no answer at all -- an older node or an unreachable one), which must NEVER collapse into refused: silence is not a decision, and a redeemer told 'refused' would stop trying a code that was never seen. THE REDEEMER RENDERS FROM THE REPLY, not from its own store, because redemption executes on the MINTING node and the redeemer holds no code record -- so the granted surfaces, the target and the acknowledgment citation ride the REDEEMED reply, while REFUSED CARRIES NOTHING AT ALL and UNCONFIRMED CLAIMS NOTHING (a refusal carrying a target would be the oracle the single message exists to prevent). THE RATE LIMIT IS THE RECEIVING NODE'S OWN, enforced against its own store and clock, and it SHARES ONE BUCKET with knock arrivals from that origin: both are attempts at the same door. A REPEAT OF THE EXCHANGE THAT ALREADY SPENT THE CODE IS ECHOED, NOT REFUSED (releases#193): the receiving node records WHICH wire exchange consumed each code -- the handshake-proven origin, the stamped redeemer and that presentation's own correlation id, plus the `reverse_requested` its first reply carried, which is the one field a later reply cannot recompute once the due is consumed -- and a presentation matching all three is answered with the SAME REDEEMED payload, reply-only: no second rule, no second minter courtesy, no second monic. This is the promise the seam's replay comment already made -- a replayed REDEEMED finds nothing unconsumed and writes nothing -- and the claim arm contradicted: measured on the two-host ladder, two dispatcher PROCESSES on one node each served one redemption, the winner consumed it and notified the minter, and the LOSER's refusal reached the presenter 88 ms ahead of the winner's REDEEMED -- so a redeemer was told 'nothing was decided and nothing was spent' about a grant written in their own name, which is the misread the outcome triple exists to prevent. The single anti-oracle message is UNCHANGED for every other cause, byte for byte, and the oracle does not widen: an echo requires proof of the exchange, which a guesser holding only the code string does not have. The dispatcher-exclusion half -- why two serve loops can exist at all -- is NOT this requirement's (releases#194). Gate: doc -- the networking entry naming the family, its tag and the outcome triple; impl -- the record, the reply, the classifier arm and the serve side; unit -- the record classifies into its own family and is never read as a message feed, an N-1 receiver drops it clean, the three outcomes are distinct with silence never rendering as refusal, refused and rate-limited emit the IDENTICAL message, and a refused reply carries no target.
2026-09-06T13:09:02.2204019Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2204199Z 
2026-09-06T13:09:02.2204313Z ### REQ-KNOCK-REDEEM-SERVE-INTENT
2026-09-06T13:09:02.2209109Z - Title: THE SERVE SIDE EXECUTES THE MINT'S STANDING INTENT, NEVER THE NODE-SOVEREIGN LEVER. A redemption arriving over the wire is answered with the authority recorded AT MINT -- owner-agent for an endpoint code, engine room for a node code, both already enforced where the code is minted -- and never `SameNodeUser`: no human is present at a wire redemption, and borrowing the node-sovereign authority would falsify the audit trail by recording a machine's act as a person's. SUBJECT BINDING MIRRORS THE APPROVAL PATH'S OWN SPLIT rather than restating it: an ATTRIBUTABLE surface binds the stamped redeemer as a sender endpoint, a NON-ATTRIBUTABLE one binds the handshake-proven origin NODE -- which is real widening, and therefore `--admit-node` ARRIVES ON `new-code`, is REFUSED for a non-attributable bake without it, PERSISTS on the code record, and is CITED when the redemption executes. That is the mechanism the glossary's 'same widening acknowledgment as an approve' promised and never had. A BARE-TERMINAL REDEEMER binds the narrow node-plus-origin-user subject, exactly as a human's knock does. Gate: doc -- the entry stating whose authority answers a wire redemption and what binds; impl -- the mint-recorded authority, the acknowledgment on the code record and the shared subject split; unit -- an endpoint code answering under owner-agent and a node code under engine-room authority with NEITHER answering under same-node-user, a non-attributable bake refused without the acknowledgment and accepted with it, the acknowledgment surviving a store round trip and being cited at redemption, and an attributable surface binding the redeemer while a non-attributable one binds the node -- the pair asserted together, since either alone passes an implementation that binds everything the same way.
2026-09-06T13:09:02.2213931Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2214093Z 
2026-09-06T13:09:02.2214202Z ### REQ-KNOCK-CODE-INERT-RULE-REPAIR
2026-09-06T13:09:02.2218516Z - Title: A SHIPPED DEFECT, REPAIRED IN THE OPEN RATHER THAN RIDDEN ALONG. `spt knock redeem` as shipped in v0.49.0 writes `Subject::SenderEndpoint` for EVERY surface and hardcodes the widening acknowledgment true at the mutation seam. For a NON-ATTRIBUTABLE surface that rule CAN NEVER MATCH TRAFFIC: it is written, reported as approved, rendered in the access view as though it governed something, and governs nothing -- the dead-rule class, and the silent arm of the same fault whose loud arm was already filed. The acknowledgment was never actually given by anyone, either; it was asserted by the code on the operator's behalf, which is the more serious half. THE REPAIR IS ONE IMPLEMENTATION UNDER BOTH ARMS -- the local verb and the wire serve side compute the grant through the SAME code, in the shape the fork-local split already established -- because a repair applied to one arm is a defect scheduled to return through the other. IT IS NAMED IN THE WAVE CARD AS A REPAIR OF SHIPPED BEHAVIOUR, not folded silently into the feature that happened to touch it: an operator whose access view currently shows an inert rule deserves to find the reason under its own heading. Gate: impl -- the shared grant construction replacing the every-surface sender-endpoint write and the hardcoded acknowledgment; unit -- a non-attributable surface redeemed LOCALLY writes a rule that MATCHES the traffic it names (asserted against the real chain, not against a second reading of it), the acknowledgment is honoured rather than assumed, and the local and wire arms produce the identical grant for one fixture -- the discriminator that proves it is one implementation and not two that currently agree.
2026-09-06T13:09:02.2223551Z - Required stages: impl, unit
2026-09-06T13:09:02.2223704Z 
2026-09-06T13:09:02.2223827Z ### REQ-HAZARD-CODE-SEAL-SEED-ORACLE
2026-09-06T13:09:02.2227634Z - Title: THE CODE SEAL MUST NEVER BECOME AN ORACLE AGAINST THE SUBNET SEED. The seed is live credential material with an existing TOTP use, and the sealed code is a string DESIGNED TO BE HANDED AROUND OUT OF BAND -- so the envelope reaches populations the seed never should. The invariant: the seal touches the seed ONLY through HKDF-derived keys under domain-separated, VERSIONED labels distinct from every other derivation the seed already feeds, so possession of any number of envelopes yields nothing about the seed and nothing about any other key derived from it. THE RAW SEED NEVER ENTERS THE ENVELOPE, the cipher key and the authentication key are SEPARATE derivations rather than one key used twice, and derived key material is wiped after use in the shape the membership-key primitive already established. This is the KDF no-reveal invariant ADR-0054 records, stated as a hazard because it is the kind of property that survives review and dies to a later refactor that 'simplifies' two derivations into one. Gate: impl -- the domain-separated derivations and the wipe; unit -- the encryption and authentication keys derived from one seed are DIFFERENT (the assertion a collapsed refactor fails), the derivation label is versioned and distinct from the membership-key and seed-proof domains, and no envelope byte equals any seed byte run for a real seed -- with a contrasting fixture proving the scan can see a match it is looking for.
2026-09-06T13:09:02.2231724Z - Required stages: impl, unit
2026-09-06T13:09:02.2231881Z 
2026-09-06T13:09:02.2231986Z ### REQ-PICKER-LIST-SCROLL
2026-09-06T13:09:02.2241404Z - Title: EVERY LIST VIEW IN THE ENDPOINT PICKER SCROLLS TO KEEP THE CARET VISIBLE. A selection the operator cannot see is a selection they cannot make: when a list is longer than the terminal, moving the caret past the last drawn row currently walks it off the bottom of the screen, leaving the highlighted row unrendered while the keys still move it — so the picker is taking input about a choice it is not showing (releases#65, reported from the field with a screenshot). THE REQUIREMENT IS ON ALL LIST VIEWS, NOT THE ONE THAT WAS REPORTED. Stated that way at mint because the reporter's own words ask for it and because the failure is structural rather than incidental: every view that draws a bounded window over an unbounded list has the same gap, so fixing the reported view alone leaves the identical defect behind every other list a later operator happens to fill up, with nothing failing until they do. A SCROLLED VIEW IS ONE CONCERN AND BELONGS IN ONE PLACE — a single primitive every list view draws through, never a per-view calculation repeated until one of them is subtly different. THE MECHANISM IS STATED BY REPLACEMENT AFTER RECON (this sentence previously described a hand-rolled offset derivation over (caret index, item count, visible rows), which was written before the render path was read and is NOT what the picker's shape calls for): the views render with ratatui, and a plain stateless list widget has no offset at all — it draws from the first item and stops at the bottom of the area, which is exactly why a caret below that line is never drawn while the keys keep moving it. The primitive therefore HANDS THE CARET OVER AS LIST STATE and lets the widget scroll to keep it visible; re-deriving the offset by hand beside a widget that already does it correctly would be a second implementation to keep in agreement, which is the drift this requirement exists to prevent rather than an instance of preventing it. WHAT THE PRIMITIVE MUST BE GIVEN IS THE CARET'S ROW AMONG THE RENDERED ITEMS, NOT A MODEL CURSOR, and this is the subtle half: the endpoint list interleaves group headers and subnet sub-lines as NON-SELECTABLE display rows, so the caret's row index runs ahead of the cursor by however many headers precede it. Handing over the cursor would scroll short by exactly the number of groups above the caret — invisibly correct on an ungrouped list, and wrong on the only list long enough to need scrolling at all. The caret stays visible at BOTH ENDS and across every way it can move. Gate: impl — the one caret-aware draw primitive, and EVERY picker list view going through it (asserted by there being no stateless list render left); unit — over a viewport deliberately SHORTER than the list, the caret's row is drawn with the caret at the first row, the last row and each step across both boundaries, the grouped list still reaches its last row (the header-offset property, which a cursor-indexed implementation fails), and list views beyond the reported one are covered too; the evidence is buffer-level through the picker's existing TestBackend seam, which is this module's established gate, so no separate int stage is owed.
2026-09-06T13:09:02.2250348Z - Required stages: impl, unit
2026-09-06T13:09:02.2250516Z 
2026-09-06T13:09:02.2250611Z ### REQ-RING-BLOCKS
2026-09-06T13:09:02.2264677Z - Title: `spt ring` BLOCKS until the reply arrives or the wait elapses, in the arms where blocking is the honest behaviour. Ringing is a question, and a verb that returns instantly having only DISPATCHED the question makes the caller invent their own wait — which is the shape the request was filed against (releases#39). THE SCOPE IS BLOCKING ONLY. The wait UNIT and its DEFAULT VALUE are releases#59 and stay OUT of this requirement entirely: they are a separate ratification and folding them in would let one change smuggle a timing default past the gate that ruled on behaviour. THE ARMS ARE MAPPED AND THE MAP IS PART OF THE CONTRACT, recorded here so a later reader does not re-derive it wrongly from the symptom. THE MAP IS STATED BY REPLACEMENT: an earlier three-arm reading of this code (the builder's own, posted to the issue and carried into the dispatch) said flatly that 'a perchless caller already blocks', and that is TRUE ONLY WHEN THE TARGET IS LISTENING AT THAT INSTANT. It is corrected here rather than annotated, because that sentence is what made the report look like it might be about expectations rather than a defect. THE ARM THAT WAS MISSING IS THE ONE THE REQUEST IS ABOUT: a perchless caller takes `Occupancy::Vacant` and builds its ephemeral reply perch, then `deliver` attempts a live send and FALLS BACK TO THE SPOOL whenever that fails — which is not merely the offline case but the ordinary state of a WORKING agent that is mid-turn, the same condition that makes `spt send` report QUEUED. On that path ring used to tear the ephemeral perch down (`remove_dir_all`) and return immediately, DELETING THE ONLY ADDRESS THE REPLY COULD ROUTE BACK TO before any reply could exist. So the reply was unreachable by construction — literally the report's 'a reply can never be processed' — and the same command had opposite semantics for the same caller depending on whether the callee happened to be mid-turn. THE FIX IS THAT A SPOOLED DELIVERY WAITS EXACTLY LIKE A LIVE ONE, entering the same loop, with cleanup still running on every exit. The loop needed no change to cover it: it already drains the perch's SPOOL as well as polling the listener, which is the leg a late reply arrives on. This touches NEITHER ruled constraint — `Vacant` means there was no perch to adopt so the adoption guard is not on this path, and the caller has no live listener to race. THE TIMEOUT OUTCOME MUST SAY WHICH SILENCE IT GOT: a spooled message that was never collected is a different event from a live message that was read and not answered, and a caller told 'did not reply' about mail nobody has picked up draws the wrong conclusion about the peer. The two are therefore SEPARATE OUTCOMES rather than one flag. The superseded `Queued` outcome is KEPT UNCONSTRUCTED rather than deleted: removing it is a public API change with nothing to do with this defect, and the wait-unit request may want exactly that shape back as an explicit no-wait mode. TWO ARMS ARE NON-BLOCKING FOR REASONS THAT SURVIVE THIS CHANGE, and neither is a defect to close: (1) `Delivered` — the caller HAS a live listener, and blocking in the ringing process would race the listener that is already going to receive the reply, so the reply is delivered where the caller is actually listening rather than to a second waiter competing for it; (2) `PerchExists` / `StaleDir` — these are held by REQ-HAZARD-RING-PERCH-ADOPTION, where adoption once deleted a LIVE endpoint AND its spool. THAT GUARD IS NOT THE BUG AND MUST NOT BE REMOVED TO SATISFY THIS REQUIREMENT — it is named here in as many words because deleting it is the shortest path to a green demo of blocking and it would re-break an invariant already paid for once. ALL THREE NON-BLOCKING ARMS ALREADY PRINT EXPLANATORY STDERR (verified in `cmd_ring` at recon), so this requirement adds no diagnostics and must not be read as asking for any. Gate: impl — the spooled path entering the existing wait with cleanup on every exit, the distinct not-yet-collected timeout outcome, and the adoption guard untouched; unit — a reply arriving inside the wait returns it, a LATE reply on the spooled path (the arm where the reply used to be unreachable) still reaches the caller, a spooled wait that elapses returns the not-yet-collected outcome and is asserted NOT EQUAL to the live timeout outcome so the two cannot be collapsed later, and each non-blocking arm is asserted to STILL not block and to still carry its explanatory stderr (the regression rows that keep a later 'make it always block' from eating the hazard guard); int — the real binary rings a real perch whose peer answers LATE, and the reply reaches stdout, with the elapsed time asserted to show it both waited and stopped waiting on the reply rather than on the deadline.
2026-09-06T13:09:02.2276914Z - Required stages: impl, unit, int
2026-09-06T13:09:02.2277077Z 
2026-09-06T13:09:02.2277182Z ### REQ-RING-TIMEOUT-MINUTES
2026-09-06T13:09:02.2282906Z - Title: `spt ring --timeout` IS DENOMINATED IN MINUTES, DEFAULT 30, WITH AN EXPLICIT SUFFIX STILL ACCEPTED (releases#59, CHANGE; ruled 2026-08-02). A ring is a question put to another agent, and an agent answers on agent time -- a 60-SECOND default expires while the peer is mid-turn, so the verb that exists to WAIT for a reply routinely reported a timeout against a peer that was about to answer. A bare number therefore reads as MINUTES, and the parser ALSO accepts an explicit s or m suffix (90s, 2m) so sub-minute waits remain expressible. Ruled and accepted with the change: callers written against the old second-intent get a longer wait, which is low risk because the failure mode of over-waiting is patience and the failure mode of under-waiting was a false negative about a peer. THE UNIT MUST FOLLOW THE VALUE ALL THE WAY TO THE SURFACES THAT REPORT IT: both no-reply arms -- the timeout arm and the spooled-but-uncollected arm -- state what was ACTUALLY waited, suffix-aware, rather than a hardcoded s, or the same wait gets reported in two units by two outcomes of one command. THE PARSE LIVES IN EXACTLY ONE PLACE, taking the argument from a bare integer to a parsed duration value, because a second parse site is how two spellings of one rule are born; garbage is REFUSED rather than silently defaulted, since a value the caller typed and the tool ignored is a wait nobody chose. IN-REPO CALLERS AND DOCUMENTED EXAMPLES REPIN WITH THE SEMANTICS, and this is required text because the defect it prevents is invisible in review: an existing bare 30 written as a 30-second ceiling becomes a 30-MINUTE one, so the ring end-to-end test loses the very bound it exists to enforce and a red arm parks instead of failing. The help sentence, the argument doc, the how-to example and the generated CLI reference all state the new unit and default; the public-help gate applies to the new wording. Gate: impl -- the single suffix-aware parse, the minutes default, the duration threaded to the wait, and both no-reply arms printing the true wait; unit -- a bare number is minutes, an s suffix is seconds, an m suffix is minutes, garbage is refused, and the no-reply arms render the wait they actually took in the unit the caller expressed.
2026-09-06T13:09:02.2288866Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.2289102Z 
2026-09-06T13:09:02.2289202Z ### REQ-FORK-CONTROL-SURFACE
2026-09-06T13:09:02.2303827Z - Title: Forking an endpoint is reachable from another node, as a CONTROL SURFACE in the ratified sense — 'a named remote-reachable operation class on an endpoint' (CONTEXT.md, access-control grill 2026-07-28). Today `spt fork` is local-only and says so: a source that is not on this node is refused outright. THE SURFACE ID IS MINTED, NOT INVENTED, and the distinction is the whole of the design fork raised before building: the control-surface vocabulary is RATIFIED OPEN — 'new surfaces mint ids without a schema change', and 'later waves mint their ids when the capability itself is built' — so a one-row addition to the surface table is the extension MECHANISM the model was given, not a new grant vocabulary. Nothing else in the access model moves: no new tier, no new subject kind, no new authority, no new decision; the grant is expressed entirely in the existing (target endpoint x surface x subject) shape, and the chain, the modes, the fail-closed degrade and the mutation seam are untouched. GATING IT UNDER AN EXISTING SURFACE IS REFUSED ON THE RECORD rather than left unconsidered — it is the only way to add the capability without minting an id, and it is the W3 dishonesty class exactly: a fork copies an endpoint's ENTIRE MIND (live context, role, every project tier, every monic), so folding it under SUSPEND or XFER would mean an operator who granted 'you may suspend my agent' or 'you may transfer files' had silently also granted 'you may clone my agent's whole mind'. A grant that confers an authority its name does not describe is worse than a refusal, and REQ-ACL-SURFACE-VOCAB exists precisely because ADR-0009's all-or-nothing whitelist did this. THE WIRE RECORD IS `kind`-TAGGED, on the knock precedent and for the same N-1 reason rather than for symmetry: the untagged `WanMessage` classification sniffs `target`+`op_id` on a stream's first line, so an untagged fork request would be claimed by that arm and DELIVERED INTO SOMEBODY'S SPOOL AS A CHAT MESSAGE by a receiver too old to know the verb. Tagged, it lands in the dispatcher's tagged arm, which an N-1 daemon resolves to the unknown family and DROPS CLEANLY — a fork request that never arrives at an old node is the correct degrade; one misread as a message is not. That degrade is PINNED BY TEST, not assumed. The new stream family must also be learned by the family census (`StreamFamily::ALL`) and every exhaustive walk over it — named here because a family added to the enum but not to the census is precisely the assembly gap that had to be repaired once already. THE REMOTE ARM NEVER DELETES THE SOURCE. `--delete-source` stays a local-only flag: deleting an endpoint is not forking, so carrying it would put two authorities under one grant (the same fault the surface split exists to prevent, re-committed inside a single verb), the destructive half is unrecoverable where the additive half is not, and the local path's 'the fork is complete; re-run to delete' degrade assumes an operator standing there, which a wire caller is not. AND THE CLI REFUSES `--delete-source` AGAINST A REMOTE TARGET LOUDLY RATHER THAN TRIMMING IT (ruled): silently forking-without-deleting honours half an invocation, which is prescribe-then-mislead in miniature — the operator asked for one thing, got another, and was not told. Refuse, do not trim. Gate: impl — the surface id, the tagged wire record with its decoder and one-shot reply, the new stream family classified and routed, the census learning it, the target-side serve that performs the fork where the source lives, the client arm that dials the resolved node, and the loud remote-plus-delete-source refusal; unit — the record round-trips and carries its tag, an N-1 receiver classifies it unknown and drops it (the anti-spool row), the family census contains it, the remote-plus-delete-source invocation is refused with nothing forked, and a local fork is unchanged; int — the receiving side end to end over REAL stores: an admitted request produces a whole new endpoint on the node that holds the source (perch record, home subnet, adapter carried, and the source's monics riding the mind), the source is left intact, and a request from a node admitted only for a NEIGHBOURING surface produces nothing and answers nothing. THE INT ROW IS STATED BY REPLACEMENT AND ITS LIMIT IS NAMED RATHER THAN GLOSSED: it was minted as 'driven across a real node boundary', and what is proven is everything on the receiving side EXCEPT the QUIC hop itself. The hop is unit-covered where its risk actually lives — the tagged record round-trips, is never classified as the message family, and an N-1 daemon drops it — and a genuine two-node rung belongs on the env-gated two-host rig, which does not run in ordinary sweeps. Claiming the boundary here on the strength of a same-process test would be the promise this project asks for evidence instead of.
2026-09-06T13:09:02.2316261Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.2316434Z 
2026-09-06T13:09:02.2316530Z ### REQ-FORK-ACCESS-GATED
2026-09-06T13:09:02.2324165Z - Title: A wire-borne fork is refused unless the access chain admits it, gated on the SAME (target endpoint x surface x subject) shape as every other control surface and on the HANDSHAKE-PROVEN origin node — never on any field the request carries. The target endpoint is the SOURCE being forked, because that is the endpoint whose mind is about to be copied and therefore the endpoint whose owner's grant is the one that matters. THE SURFACE IS `attributable: false` FOR THIS WAVE, and it is a deliberate restriction rather than an oversight: only families carrying a daemon-stamped, session-proven sender endpoint may be attributable, and making fork the second such family would change WHICH SUBJECT TIER can express a fork grant — sender-endpoint rules would become real for it, not just node-tier ones — which is a second decision riding a wave that was scoped to one. So node-tier subjects govern fork, a sender-endpoint rule naming it is dead on arrival exactly as the table's restrictive default says, and the flip later is a ONE-ROW EDIT with no re-ruling, through the same door XFER already stands in front of. Because the surface table is the single source for that property (and reads case-insensitively since releases#66), a future flip cannot leave one reader disagreeing with another. A REFUSAL FORKS NOTHING AND SAYS NOTHING BACK beyond the family's existing refusal posture — the attach/rest precedent, where existence is not advertised to an unadmitted caller. THE SYNERGY WITH releases#66 IS STATED SO IT IS NOT MISTAKEN FOR LUCK: because `knock list` now composes its prescription from the same predicate the approve path decides on, a knock requesting FORK prescribes the whole-machine acknowledgment correctly from the day the surface exists, with no per-surface work — a non-attributable surface binds a node subject, which is the widening form, and the listing says so because it asks rather than because someone remembered to teach it. Gate: unit — a node the chain admits for FORK is served while one it does not is refused with nothing created, a grant on a DIFFERENT surface does not admit the same node to fork (the per-surface isolation property the vocabulary exists for), the gate reads the proven origin rather than any claimed field, and a fork grant expressed as a sender-endpoint rule does not admit (the attributable:false consequence, asserted so the restriction is evidenced rather than assumed).
2026-09-06T13:09:02.2330818Z - Required stages: doc, unit, int
2026-09-06T13:09:02.2331034Z 
2026-09-06T13:09:02.2331140Z ### REQ-ER-BRINGUP-SPAWNS-SESSION
2026-09-06T13:09:02.2355692Z - Title: Passing the engine room's bring-up gate BRINGS THE ENGINE ROOM UP: the node spawns its session through the adapter bound at the ceremony, and the operator's attach then lands on it. Today it does not, and the endpoint is entirely inaccessible (releases#71, operator-reported): the ceremony provisions a record, the gate is complete and correct, and NOTHING ANYWHERE SPAWNS THE SESSION — `BringUp::Admitted` briefs and returns proceed, and the attach falls through to the ordinary no-live-session refusal. THE SPAWN LIVES DAEMON-SIDE (ruled): the broker owns hosting, so a client-side spawn would replicate hosting logic in the CLI and race the attach that follows it; the daemon already launches harnesses on its own behalf at autostart, which is the shipped precedent for the mechanism and for its threading. The attach that follows the spawn WAITS FOR READY UNDER A BOUND — a wait with no bound turns a broken adapter into a hung terminal, and no wait at all reintroduces the same no-live-session refusal one race later. THE ENTRY POINT IS PART OF THIS REQUIREMENT AND IS STATED HERE BECAUSE IT IS WHERE THE DEFECT ACTUALLY BITES: with no session hosted, `spt rc engine-room` never subscribes at all — it resolves no local session, resolves no owning node for an endpoint that is deliberately never advertised, and returns the reported 'No live session' before any code reaches the daemon, so the typed code is verified NOWHERE and the Admitted arm is unreachable in exactly the flow the operator ran. A fix that adds a spawn leg to that arm without giving a session-less bring-up a way to reach the gate closes nothing. VERIFICATION STAYS DAEMON-SIDE WHEREVER THE ENTRY LANDS: the adversary in this threat model is an agent on the box, which can speak the broker's IPC and never run a line of the CLI, so a client-side check is a courtesy and the rule must be evaluated where the session is hosted. THE ENTRY IS AN ADDITIVE BROKER REQUEST carrying the code (ruled): the daemon runs the same classifier it already owns, spawns on admit, and replies the session the operator then attaches to. Additive is the ADR-0001-safe shape, and an N-1 broker that does not know the verb answers unknown — WHICH THE CLIENT REPORTS HONESTLY IN THE OPERATOR'S OWN WORDS, naming the version skew, and NEVER falls through silently to the resolve path that produces the very no-live-session refusal this requirement exists to kill. The spawn RUNS OFF THE REQUEST HANDLER on its own thread, following the autostart replay's shipped shape, because the launch dials the broker as a client and must not run inline inside the handler serving that request. THE GATE IS SPENT ONCE PER BRING-UP, and the deciding argument is NOT budget arithmetic but the TOTP WINDOW RACE: the bounded spawn wait can outlive the ±1 ceremony window, so a code re-presented at the following subscribe can be REFUSED by the same gate that just admitted it — charging the human a ledger failure and rebuilding gate-admits-then-attach-refuses one seam later, which is the exact shape #71 exists to kill. The admitted bring-up therefore mints a SINGLE-USE ADMIT TICKET for the subscribe that seats the controller: a random secret of at least 128 bits, returned in the reply, SCOPED TO THE SESSION ID the bring-up spawned, and EXPIRING 30 SECONDS AFTER THE REPLY. A CONNECTION-KEYED exemption was ruled first and is STRUCTURALLY UNREDEEMABLE at this topology, which is why the key is a ticket and why that is stated here rather than left as an implementation choice: an rc attach opens a net stream, and the subscribe that seats the controller is sent by the DISPATCHER'S OWN broker connection, never by the connection that asked for the bring-up, so the two connection ids can never match and a conn-keyed exemption could never fire at all. The key must be the thing that actually crosses the seam, and the attach's code field is already the end-to-end pipe for exactly this class of secret, so the ticket rides that field behind a reserved `admit:` prefix and no new wire field appears. THE PREFIX IS RESERVED OUT OF THE TOTP PATH STRUCTURALLY: a prefixed string is never evaluated as a code guess, because an expired ticket is not a wrong guess and must not spend a unit of the human's attempt budget — which also stops anyone on the box from ratcheting the human toward a backoff by replaying a dead ticket. The ticket is consumed BY PRESENTATION rather than by success, so a second presentation is refused even inside its window and a ticket presented for another session is spent as it is refused; expired, already-spent, wrong-session and never-minted all answer ONE uniform sentence, so a caller learns only that this string does not seat it and never whether an admit is outstanding. The TTL starts at the REPLY — after the spawn, after the bounded wait — so it covers only the client's attach turnaround and never a harness start. Entropy, TTL and single use are the ticket's whole defense, because the field it rides spends no ledger. EXEMPTION PRECEDENCE, ADDED FOR releases#203 (doyle ruling 2026-08-20): the seated-connection re-serve exemption is evaluated BEFORE the presented ticket, and that ORDER is part of the requirement rather than an implementation detail — because the ticket this requirement mints is SINGLE-USE and the connection it seats re-presents it on every re-subscribe. With the ticket matched first, a gap resume or dispatcher reconstruction of a ticket-seated controller answered `Unredeemed` and the refusal UNSEATED the human mid-session; the seat this requirement exists to establish was then lost to the very credential that established it, and the exemption was structurally unreachable for any re-serve carrying its spent ticket. Measured on golden 32341702157 (Linux leg): seat on a redeemed ticket, the SAME conn re-serving ~700ms later with `req_gen` unchanged, `engine-room-admit-unredeemed`, `session-detach was_controller=true`. Latent since the ticket shipped and first witnessed by a new e2e, not by a report — the availability half of this requirement had no same-connection witness at all. THE ORDER IS NOT A WIDENING, and the reason belongs in the requirement because it is what makes the precedence safe to state: the exemption's discriminant is the seated CONNECTION id, which belongs to another process's socket and cannot be claimed by asking, so the only attach it admits on a spent ticket is one arriving on the connection ALREADY SEATED — granted nothing it does not already hold, and dying with that connection. A ticket presented by any other connection still meets the match and is refused on its own merits, ledger untouched. Gate: doc — the engine-room setup guide's bring-up section, stating that the first bring-up spawns the session and what a bounded wait failing looks like; impl — the daemon-side spawn on admit through the ceremony-bound adapter, the bounded wait-for-ready, and the session-less entry path that carries the code to the gate; unit — an admitted bring-up spawns exactly once through the BOUND adapter, a refused one spawns nothing, a bring-up over an already-live session spawns no second session, the wait's bound is proven to be a bound (an adapter that never reports ready ends in a typed failure rather than a hang), one bring-up spends exactly one ledger attempt, the admit ticket is proven to seat the controller ACROSS THE REAL TWO-CONNECTION TOPOLOGY (the row that would have caught a conn-keyed exemption before it shipped), a SECOND presentation of the same ticket is refused, an EXPIRED ticket is refused WITH THE ATTEMPT LEDGER UNTOUCHED, a ticket presented for a DIFFERENT session is refused, and every non-redeeming presentation answers the SAME sentence so the refusal discloses nothing, a SPENT ticket re-presented by the connection that is ALREADY SEATED keeps that seat (the same-connection gap-resume row, pinned by an explicit conn-id assertion and a FIXED attach generation so a fresh-connection re-serve cannot masquerade as it), and that same spent ticket presented by a DIFFERENT connection is still refused with the ledger untouched (the non-widening control, without which deleting the refusal arm outright would satisfy the row above); int — a provisioned engine room with a registered adapter comes up end to end from the invocation the operator runs and the controller is seated on the session that was spawned.
2026-09-06T13:09:02.2377481Z - Required stages: impl, unit, int
2026-09-06T13:09:02.2377643Z 
2026-09-06T13:09:02.2377752Z ### REQ-ER-RESERVED-ID-SPAWN-REFUSAL
2026-09-06T13:09:02.2386472Z - Title: THE ENGINE ROOM'S ID IS RESERVED AT EVERY VERB THAT MINTS AN IDENTITY, and this is a BYPASS rather than a naming nit (ruled): `spt endpoint run engine-room` mints the engine-room identity and hosts a session under it WITHOUT EVER PASSING THE TOTP GATE, because the gate guards the ATTACH and not the NAME. An agent on the box that can spawn the reserved id owns the surface that sets the node's access posture, which is precisely the authority the bring-up ceremony exists to price. Every spawn-family verb therefore refuses the reserved id outright and NAMES `spt rc engine-room` AS THE ONLY ENTRY, so the refusal teaches the correct path instead of inviting a workaround — a reasoning agent told only 'no' invents a route, which is the prescribe-then-mislead lesson turned around. THE REFUSAL GETS A CANARY UNIT PER SITE, and that is required evidence rather than diligence: a capability limit that lives only in prose or only in a prompt is not a limit, and the canary is what makes the prohibition tool-layer-enforced (the standing lesson from the strike-and-amend class). THE FAMILY IS WIDER THAN THE VERB THE REPORT NAMED, and enumerating it is part of the requirement because a refusal at one door with the rest left open reads as closed while remaining open: the direct `endpoint run` path; the interactive picker's create-new entry, which never passes through that path's arguments; the `--save` startup default, which PERSISTS a bypass that then replays with no gate at every daemon start, so it is refused at save AND skipped loudly at replay; and the identity-minting listener verb, which binds the reserved id to a perch with no harness and no gate at all — IN because the stated harm is minting the identity without the gate, and the engine room has exactly ONE entry, its bring-up. THE FAMILY CLOSES BY ENUMERATION RATHER THAN BY THE SITES ANYONE HAPPENED TO NAME: the whole id-minting and spawn-dispatch surface is swept before the refusals are written and the site COUNT is reported with the refusal list, so a site that exists but was not thought of fails the sweep rather than shipping open. A site that cannot refuse is named in the evidence rather than left silent. THE PERSISTED-DEFAULT ARM CARRIES ITS OWN ROW, because a record written BEFORE this fix already exists on real nodes: refusing at save alone leaves those replaying ungated at every daemon start, so the replay arm must skip a pre-existing reserved-id default loudly and be tested against one. Gate: doc — the reference help for the refusing verbs and the engine-room guide's entry sentence; impl — the reserved-id refusal at every enumerated site, each naming `spt rc engine-room`; unit — a canary per site asserting the reserved id is refused and NOTHING is minted, spawned, persisted or replayed, that the refusal names the bring-up entry, that a PRE-EXISTING persisted reserved-id default is skipped loudly at replay, and that an ordinary id is unaffected at the same site.
2026-09-06T13:09:02.2394932Z - Required stages: impl, unit
2026-09-06T13:09:02.2395090Z 
2026-09-06T13:09:02.2395205Z ### REQ-ER-BRINGUP-PROMPT-NAMES-SEAT
2026-09-06T13:09:02.2401354Z - Title: THE BRING-UP PROMPT NAMES WHAT IT IS ASKING FOR: which node's engine room is being brought up, which subnet's code will be accepted, and that a MEMBER OR ADMIN code answers it. The shipped prompt asks for a 'bring-up code for engine-room (member or admin)' and names neither the node nor the subnet (releases#71, operator-reported), which leaves the human holding an authenticator app with no way to tell WHICH of the subnets they belong to this prompt means — and on a multi-subnet node that is not a cosmetic gap, it is a guess that spends a unit of a deliberately small attempt budget. The facts are always available where the prompt runs: bare `spt rc engine-room` is LOCAL-ONLY by ratification, so the node is this node and the home subnet is on the local engine-room record. The prompt reads them THERE rather than asking the daemon, because the record is the ceremony's own output and a prompt that cannot render without a round trip is a prompt that hangs when the daemon is down. THE WORDING KEEPS BOTH KEYS: an admin seed IS a membership key (ADR-0051), so 'member or admin' is the truth and narrowing it to 'member' would make the prompt lie about a code that works. This REFINES the interaction contract in REQ-ER-BRINGUP-PROMPT rather than replacing it — Esc/ctrl-c cancel, digits-only accumulation, the bound, the inert empty Enter and the non-terminal refusal all stand unchanged and are not re-litigated here. Gate: doc — the engine-room setup guide's bring-up walkthrough showing the prompt as it renders; impl — the prompt composing node and home subnet from the local engine-room record with the member-or-admin wording; unit — the composed prompt names the node and the home subnet read from the record, an unprovisioned node's prompt still refuses honestly rather than rendering an empty subnet, and the member-or-admin wording is asserted so a later narrowing fails here.
2026-09-06T13:09:02.2406723Z - Required stages: impl, unit
2026-09-06T13:09:02.2406875Z 
2026-09-06T13:09:02.2406991Z ### REQ-ER-BRINGUP-INFLIGHT-LEDGER
2026-09-06T13:09:02.2414651Z - Title: THE BROKER RECORDS A BRING-UP WHEN IT STARTS ONE, and the reserved-id bind gate reads that record as well as the sessions table. The gate's premise is that the reserved id passes only as THE COMPLETION OF A BRING-UP THE BROKER STARTED (REQ-ER-RESERVED-ID-SPAWN-REFUSAL), but the only fact it can consult is the sessions table, which records COMPLETED-ness — so between the broker creating the harness child and the session row being inserted there is a window in which the broker's OWN spawned harness asks the gate whether a bring-up it is itself completing exists, and is told no. Measured end to end on releases#199: the child's first act is `spt api bind engine-room`, the arm that answers is `ER_HOSTED_PROBE:no-row sessions=0` (dial OK, query OK, table EMPTY — named 3/3 in a channel proven to speak), the bind is refused RESERVED_ID, the harness exits bind-failed, no session ever registers, the ready-wait burns its full bound and the operator is told the engine room is not up. THE DEFECT IS A MISSING FACT, NOT A TOLERANT READER: the fix writes started-ness where it happens instead of teaching every reader of the sessions table to tolerate a counterfeit of its consequence, so the ledger lives OUTSIDE that table and the wake gate, the zombie verdict, every process-id reader and the rollback path are untouched by construction rather than by discipline. The record is endpoint-keyed with a deadline, written before the launch thread fires, swept on insert like the admit tickets, removed the moment the session row appears, and expiring harmlessly when a spawn fails so no phantom entry can outlive the bring-up that wrote it. Its TTL is the bring-up's own ready-wait, because the window it covers is exactly the window the bring-up is willing to wait through. It reaches the gate as one additive field on the brain's sessions reply — internal IPC, no P2P wire change. THE RESIDUAL IS STATED RATHER THAN ABSORBED: the added arm admits a hand-run bind of the reserved id during a live in-flight window, which satisfies the same not-as-a-first-mover premise (the broker did start a bring-up), is bounded by the TTL, and collides into the existing CONFLICT arm. Gate: impl — the endpoint-keyed in-flight ledger written pre-spawn on the bring-up path and cleared at registration, carried on the sessions reply, and the hosted predicate answering row-OR-in-flight; unit — a bring-up recorded before its session exists answers the predicate true, a registration clears the entry, an expired entry answers false, a sweep on insert drops what is no longer live, and an ordinary spawn writes no entry at all.
2026-09-06T13:09:02.2421616Z - Required stages: impl, unit
2026-09-06T13:09:02.2421759Z 
2026-09-06T13:09:02.2421873Z ### REQ-ER-BRINGUP-ANCHORS-PERCH
2026-09-06T13:09:02.2433082Z - Title: A BRING-UP THAT KNOWS THE ROOM'S ANCHOR CREATES THE ROOM'S PERCH WITH IT, so the harness bind it is about to launch inherits that anchor instead of re-resolving one it is not allowed to guess. An endpoint's anchor subnet is its DEFAULT-SCOPE subnet, so `stamp_creation_fields` refuses to invent one at creation on a node holding more than one subnet (ANCHOR_REFUSED / MULTI_SUBNET_HOME) — a deliberate explicit-choice seam that this requirement PRESERVES rather than relaxes: a caller with no anchor still cannot create a record, and the bind path still refuses exactly as before. The defect was that the engine-room bring-up HELD the answer and dropped it — `room.home_subnet` is the very value the same handler prints as ENGINE_ROOM_ADMIN_BRINGUP_EMPOWERED — so on every multi-subnet node the room's own perch was uncreatable. Measured end to end on HFENDULEAM at v0.58.0 (releases#204): no `info.json` was ever written for engine-room (the perch dir held only `spool.db` and `.has-messages`), the harness's first act `spt api bind engine-room` met the creation arm with prior=None and was refused naming all three subnets (BIGNET, SPT_MANTLE, SPT_DEV) — loudly, but only into the adapter's `hook-trace.log` — the endpoint therefore never entered any roster, `spt send engine-room` answered NO_PERCH three times across fifteen minutes, and the retained briefing row could never be delivered to a target that does not exist. The skeleton is written beside the in-flight ledger, before the launch thread fires, for the reason that entry is there: both are facts about a bring-up this broker started and both must be true before the harness exists to observe them. It is IDEMPOTENT BY REFUSAL — an existing record is left untouched, so a second bring-up cannot reset a live perch — and LOUD BUT NEVER A REFUSAL, the same denial-of-governance rule `empower_home_for_bringup` follows: a record that could not be written costs the operator the briefing, not the room, and degrades to exactly the known pre-fix path rather than inventing a new failure. SECOND SURFACE, SAME REQUIREMENT: the UNPRESENTED alarm splits, because one text was covering two events and promising what only one of them can keep. Perch-ABSENT (no record — in no roster, no inject can ever target it, the retained row waits forever) must NOT tell the operator the briefing arrives at the session's first idle; perch-SLOW (record exists, not yet a deliverable inject target) keeps today's promise, which it can honor. The absent arm fires only on a POSITIVE measurement of absence — the record file is not there — so an unreadable or unparseable record takes the promising arm (an absent answer is not a zero), and the predicate is record-EXISTENCE rather than liveness so a re-attach to a merely idle room can never false-fire it. Gate: doc — the published harness-contract sentence that the identity brief's perch claim is valid only after successful registration, and that on a refused bind the adapter emits the no-perch shape carrying the refusal (the sentence the adapter and omp-spt build against); impl — the anchored-skeleton writer in spt-store called from the bring-up path with the room's own home_subnet, plus the two-arm alarm; unit — the skeleton writer anchors a fresh record and seeds its scope, refuses to overwrite an existing one, and the alarm predicate distinguishes absent from slow with the unreadable case failing OPEN to slow; int — a REAL bring-up on a multi-subnet node leaves a rostered perch record (the pre-fix red: the record is absent and the bind is refused), which is the witness the pre-existing multi-subnet cell could never be, because its rig pre-creates the anchored skeleton that is exactly the condition the real path failed to establish.
2026-09-06T13:09:02.2442784Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.2442955Z 
2026-09-06T13:09:02.2443065Z ### REQ-UNLISTED-ANSWERED-CROSS-NODE
2026-09-06T13:09:02.2451068Z - Title: A CROSS-NODE KNOCK WHOSE ANSWER THIS NODE CAN PROVE RENDERS THAT ANSWER, AND EVERY SILENCE THAT CANNOT BE PROVEN STAYS A SILENCE. The `invited-outbound` evidence line said `we knocked them; answer unknown here (releases#87)` for EVERY cross-node knock, on the reasoning -- true when written -- that the approval receipt is produced where the notification is produced, that the notification never crosses, and that no wire carried one. THE WIRE SHIPPED (DOORBELL W5: spt-net answermsg, spt-daemon answerop, the knocker's half), so the line became a stale claim about the SYSTEM rather than a fact about this node -- and it said so in prose the same lane must correct, since three sites asserted the receipt family was `unbuilt` while the built wire sat beside them. THE FILING'S OWN MECHANISM IS WRONG IN ITS SPECIFICS AND THE MEASUREMENT IS THE RULING'S GROUND (doyle 2026-08-21): there is no `Answered` knock state (they are Pending/Approved/Denied/Expired), and the receipt does NOT land on the knock row -- `receipt_ms` has exactly one production writer, at the notification-produce site, whose own doc binds it to a knocker reachable through this node's notification store, which is to say ON THIS MACHINE. What a cross-node answer touches is the knocker's own MUTUAL PRE-AUTHORIZATION: `apply_answer_receipt` marks it consumed only after matching the keyspace, the correlation id, the answerer against the endpoint that was knocked, and the answering node against the HANDSHAKE-PROVEN origin the knocker bound at arm time. A consumed record is therefore a proven answer sitting on this node, and the render REPORTS that proof rather than re-deriving it -- it must not weaken any check the consume already made. THREE SILENCES REMAIN AND NONE MAY BE READ AS AN ANSWER, which is why the unknown line survives with corrected words (`no answer has reached this node` -- a fact about US, never `no wire exists` and never `unanswered`): an ARMED-BUT-UNCONSUMED record means the answer has not arrived; a NEVER-ARMED knock, the non-mutual majority, has nothing for a receipt to land on, so an approval leaves no trace here at all; and a DENIAL REMOVES the record (deliberately -- a consumed record means the reverse rule was written, and a merely-flagged one would read back as the approval it was not), which makes declined and never-armed THE SAME BYTES. Rendering `declined` out of that absence would manufacture a peer's decision, and an operator who read it would stop asking someone who had in fact let them in. Gate: impl -- the consumed-pre-authorization join over the store already in hand (no new read), keyed on BOTH keyspace and correlation id so a redeemed code cannot report an answer for a knock, the split cross-node arms, and the three stale `unbuilt` prose sites corrected in the same change; unit -- the proven row renders approved AND names what proves it; each of the three silences renders the unknown line with the banned vocabulary asserted absent on every one of them; a code-keyspace pre-authorization sharing the correlation id renders a silence (the keyspace half of the key, which an id-only match would lose); and the same-node arms are untouched.
2026-09-06T13:09:02.2459294Z - Required stages: impl, unit
2026-09-06T13:09:02.2459453Z 
2026-09-06T13:09:02.2459553Z ### REQ-UNLISTED-EVIDENCE
2026-09-06T13:09:02.2466292Z - Title: An endpoint that this node holds LOCAL EVIDENCE of is knowable to its own operator without a DISCOVER grant and without any change to discovery: `spt endpoint list` auto-qualifies the invoking entity and renders off-node peers drawn from three local sources, each row carrying its provenance -- invited-outbound (a knock this node sent, and the approval receipt where one exists), admits-inbound (this node's OWN access rules naming that endpoint as a subject), and recent-traffic (the contact ledger, REQ-UNLISTED-CONTACT-LEDGER). A row STATES ITS EVIDENCE AND NEVER ASSERTS LIVENESS: a stale rule can name a deleted endpoint, so the evidence is a fact about this node's own records, never a claim about the peer. TWO SILENCES APPLIED TO RECEIPTS, BINDING ON THE RENDERER: the approval receipt is persisted at the notification-PRODUCE site, which exists only for a SAME-NODE approval -- with the receipt wire family unbuilt (releases#87) a cross-node approval produces no receipt on this node at all, so an approved cross-node knock and an ignored one are the same bytes here. A cross-node invited-outbound row therefore states ANSWER UNKNOWN and names releases#87; it must never read as 'unanswered' and never as anything a reader could take for declined. A same-node row may state approved, because there the receipt exists. The outbound knock itself is persisted locally -- the pre-fix `KnockSendOutcome::Queued` arm returned before the upsert, so a cross-node knock left this node holding nothing -- which is what makes 'I knocked them, they exist at node N' real cross-node evidence. SCOPE GUARD: the retained outbound knock is evidence substrate only; whether `knock list` grows an outbound view is NOT this requirement. A bare-terminal invocation mirrors the fork knocking already named (`Knocker::User`): its asker is subject invoking-node / origin user, and the list renders the NODE's evidence. Gate: doc -- the CONTEXT.md UNLISTED-evidence entry; impl -- the three evidence sources, the produce-site receipt, and the Queued-arm persistence; unit -- a cross-node invited-outbound row says answer-unknown while a same-node one says approved (the discriminator pair), a knock sent cross-node is retained locally, and an evidence row never renders a liveness word.
2026-09-06T13:09:02.2472532Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2472694Z 
2026-09-06T13:09:02.2472798Z ### REQ-UNLISTED-CONTACT-LEDGER
2026-09-06T13:09:02.2476556Z - Title: Recent-traffic evidence rides NEW node-scoped state, and the REQ-SEC-1 recent-outbound correlation log is UNTOUCHED. A firewall's memory and a UI's memory must not be the same row: `RecentOutbound` is keyed (local endpoint, remote node) with no peer endpoint id, its window is REPLY_WINDOW_MS = 15 minutes, and it is the stateful-firewall 'established/related' input -- widening its key or its retention for a listing feature would silently widen an admit verdict, a security regression wearing a feature's clothes. THE CONSTRAINTS RIDE THIS MINT, NOT A LATER REVIEW: rows are (direction inbound/outbound, endpoint id, origin node key, last-seen); the retention window is 14 DAYS; the hard size cap is 512 ROWS, pruned oldest-first, so the ledger is bounded regardless of traffic shape; and it is NODE-LOCAL AND NEVER SYNCED -- it enters no replication feed, in the shape ADR-0046's node-scoped notif rows already established. Write sites: inbound is recorded POST-GATE in the WAN receive funnel (the sender endpoint is daemon-stamped and the origin node is handshake-proven; a message the gate refused is not contact), outbound at the send legs. Gate: doc -- the storage entry naming the window, the cap and the never-synced rule; impl -- the store with record/prune/cap and both write sites; unit -- a row older than the window is dropped, the cap prunes oldest-first at its stated VALUE, a refused inbound writes nothing, and the ledger is absent from every emitted feed.
2026-09-06T13:09:02.2480607Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2480763Z 
2026-09-06T13:09:02.2480860Z ### REQ-UNLISTED-RENDER
2026-09-06T13:09:02.2484263Z - Title: Evidence-known off-node peers render in a NEW status family, UNLISTED, which is deliberately NOT the ratified HIDDEN vocabulary: HIDDEN names an operator-chosen exclusion (REQ-INST-12) and those rows must never read as reachable, so reusing the word would collapse two opposite meanings onto one surface. Rows are provenance-labelled with the source that produced them. COLOUR NEVER CARRIES THE MEANING ALONE -- the status WORD rides the row and `--json` carries the state name, so a colour-blind reader, a piped log and a machine consumer all read the same fact. BOTH SOURCE LEGS OF `endpoint list` CARRY THE FAMILY: the human path and the `--json` path are separate constructions over the same facts, and the reader-parity discipline REQ-LIST-JSON-LIVENESS-PARITY already enforces there applies unchanged -- a family that appears on one leg only is the parity defect that requirement exists to prevent. Gate: doc -- the CONTEXT.md status-vocabulary entry distinguishing UNLISTED from HIDDEN; impl -- the family, its provenance labels and both render legs; unit -- an UNLISTED row renders its status word with colour disabled, a HIDDEN endpoint never renders as UNLISTED, and the human and `--json` legs agree on the same fixture.
2026-09-06T13:09:02.2487779Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2487932Z 
2026-09-06T13:09:02.2488033Z ### REQ-UNLISTED-EVIDENCE-ROUTE
2026-09-06T13:09:02.2491853Z - Title: A row the operator can see but cannot act on is a worse surface than no row, so an evidence-known peer is REACHABLE BY SEND: when the registry holds no row for an endpoint -- `resolve_among` returns NotFound on an empty live set even for an explicit @node pin -- but the evidence holds (id, origin node key), the send dials THAT NODE directly, on the node-tier address that replicates to every subnet member regardless of endpoint DISCOVER. THE TARGET'S ACCESS GATE STAYS THE SOLE AUTHORITY ON ADMISSION, exactly as it is for every inbound today: routing on evidence is not circumventing hiddenness, because the invitation IS the consent -- the sealed-code route-rides-evidence logic applied to ordinary send. THE ASKER'S OWN VISIBILITY EXCLUSIONS STILL REFUSE, AND THE REQ-INST-12 BOUNDARY IS RE-IMPOSED WITH THE SAME PREDICATE THE RESOLVE PATH RECEIVES -- one shared closure, never a copied condition: the exclusion check lives INSIDE `resolve_visible`, which this route bypasses by construction, so a second hand-written condition would be a boundary that drifts silently from the one it claims to mirror. Gate: doc -- the networking entry stating what evidence routes on and what it does not widen; impl -- the evidence-route leg and the shared exclusion predicate; unit -- the discriminator pair, an excluded id REFUSED through the evidence route and a non-excluded one ADMITTED, plus a refusal that names the boundary rather than reading as a missing endpoint.
2026-09-06T13:09:02.2495751Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2495905Z 
2026-09-06T13:09:02.2496014Z ### REQ-KNOCK-EVIDENCE-ROUTE
2026-09-06T13:09:02.2502817Z - Title: A KNOCK REACHES AN EVIDENCE-KNOWN PEER THAT RESOLUTION CANNOT FIND, exactly as an ordinary send already does. `knock_send_remote` returned `NotFound` the moment `resolve_across_visible` did, while the message leg at the SAME resolution point fell through to the node-tier evidence route (REQ-UNLISTED-EVIDENCE-ROUTE, DOORBELL W3): an undiscoverable target was therefore UNKNOCKABLE WHILE STILL SENDABLE -- the one door whose whole purpose is to be knocked on was the one that was closed, and a knock is precisely how a stranger asks to be admitted. IT IS ALSO THE N-1 INTEROP LEG OF releases#180 AND LOAD-BEARING, NOT COSMETIC: a peer on a pre-#180 build still applies its blanket-closed posture to DISCOVER and will not advertise, so flipping OUR default cannot make THEM knockable, and without this arm #180 buys nothing against the installed fleet. THE ARM IS THE MESSAGE LEG'S, NOT A SECOND MECHANISM: the same `unlisted::route_node_for` over the same `raw_evidence`, consuming the same field (the node key) the message leg consumes, and the ASKER'S OWN EXCLUSION IS RE-IMPOSED AT THE ARM with `excl.anywhere()` -- the same source the resolve path's predicate came from -- because resolution's own check lives INSIDE the call this arm has already fallen out of. NOTHING ABOUT ADMISSION MOVES: the TARGET's access gate rules this hop at the owner's own seam exactly as it rules every inbound today; routing on evidence is not circumventing hiddenness, because the invitation IS the consent. `KnockSendOutcome::Excluded` is minted as the message leg's `WanSendOutcome::Excluded` sibling so an exclusion refusal NAMES THE BOUNDARY AND WHOSE IT IS instead of collapsing into `NotFound` and sending the operator hunting at the far end for a decision they made at this one -- and it is deliberately NOT a landing: it must not take `NotFound`'s local-inbox fallback, since recording the ask locally would write a row against a boundary the operator themselves drew (releases#145's defect in a new costume). Gate: doc -- the CONTEXT.md evidence-route entry extended to name the knock leg and what it does not widen; impl -- the NotFound arm on the knock leg, the `Excluded` outcome, its CLI line, and its placement in the landing/refusal partition; unit -- the discriminator triple at the real function over an EMPTY registry (no evidence still refuses NotFound, an excluded id refuses through the route, and a non-excluded evidence-known id is REACHED), with the excluded arm run FIRST so the admitted arm cannot be a leftover, plus `Excluded` proven to be neither a landing at EITHER value of target-locality nor a wire refusal.
2026-09-06T13:09:02.2509834Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.2510002Z 
2026-09-06T13:09:02.2510110Z ### REQ-UNLISTED-PRESENCE-PROBE
2026-09-06T13:09:02.2515349Z - Title: `endpoint list` answers 'is this peer there right now' for UNLISTED rows with a presence probe, and the probe is NOT A MESSAGE: it is its own kind-tagged wire family, in the shape KNOCK_KIND already established, so an N-1 daemon classifies it Unknown and DROPS IT CLEAN. Probes fan out simultaneously and the verb BLOCKS on them under a HARD 10-SECOND PER-PROBE CEILING passed as a PARAMETER -- the shared PROBE_TIMEOUT const stays 2500ms and is untouched -- over the `probe_all` + `run_bounded` fan-out shape, so the wall-time is one ceiling and one wedged dial never drags the batch. TWO SILENCES: a probe that goes unanswered renders UNKNOWN AND NEVER OFFLINE, because an N-1 daemon's clean drop and a dead node produce the same silence. The answer distinguishes LISTENING / BUSY / OFFLINE, and each word has exactly one authority: they are answered from the serving daemon's HONEST SESSION AUTHORITY (the broker's hosted-session map and its activity window), NEVER from perch records -- the derived-view class where `is_online` reads a ready-file and a liveness stamp. BUSY IS A BROKER-AUTHORITY WORD ONLY. A relay-hosted peer holds no broker session, so it answers from a real connect attempt: accepted is LISTENING, and anything else is UNKNOWN -- busy and offline are indistinguishable there, and rendering offline would be a coin flip presented as a fact; the renderer may say why. CONSENT GROUNDING: the target node answers only if its own rules admit the asker for MSG to that endpoint -- enforcement stays owner-side at the owner's seam -- and presence already leaks to exactly this population one message at a time (SENT vs QUEUED), so the probe adds no new information class. Gate: doc -- the networking entry naming the family, the ceiling and the three-plus-unknown vocabulary; impl -- the record, the dispatcher arm, the serve side and the bounded fan-out; unit -- an unanswered probe reads unknown and not offline, a relay-hosted peer never reads busy, an unadmitted asker is refused at the owner's seam, and the ceiling is honoured per probe rather than per batch.
2026-09-06T13:09:02.2520858Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2521018Z 
2026-09-06T13:09:02.2521127Z ### REQ-HAZARD-PROBE-CONTEXT-ISOLATION
2026-09-06T13:09:02.2523629Z - Title: A PRESENCE PROBE MUST NEVER REACH THE RECIPIENT AS CONTENT. It is never spooled, never lands in an inbox, and never enters the recipient's agent context -- the whole point of a machine-plane question is that the machine answers it and the human or agent behind it is not interrupted to do so. The hazard is the one KNOWN-HAZARDS already records for untagged wire records, in a new dress: a probe that presented the untagged message signature would be classified a message feed by an older receiver and DELIVERED INTO THE TARGET'S SPOOL, turning a liveness question into a chat line nobody sent. The kind tag is what keeps it out of that arm, and an N-1 daemon drops it clean. Gate: impl -- the tag and the serve arm that answers without any delivery side effect; unit -- the probe record can never be read as a message feed (the classifier's own inputs, asserted), and a served probe leaves the target's spool and inbox byte-identical to before it arrived.
2026-09-06T13:09:02.2526282Z - Required stages: impl, unit
2026-09-06T13:09:02.2526429Z 
2026-09-06T13:09:02.2526537Z ### REQ-HAZARD-MUTUAL-PREAUTH-SIBLING
2026-09-06T13:09:02.2531754Z - Title: A DEDUPE KEY NARROWER THAN THE RECORD'S IDENTITY SILENTLY DESTROYS A SIBLING. Re-arming a mutual pre-authorization REPLACES rather than stacks, so a re-knock cannot accumulate duplicate reverse grants -- but the identity that replacement tests is `(kind, key_id, OWNER)`, never `(kind, key_id)`. A single code id is legitimately shared by TWO pre-authorizations with different owners: the MINTER's, armed at mint, whose subject is empty because no redeemer exists yet, and the REDEEMER's own, armed at redemption, whose subject is known. Keyed without the owner, arming the second would find the first and overwrite it in place, and one side of an agreed two-way would simply VANISH -- no error, no refusal, no second grant to notice missing; the surviving side would look entirely correct on its own. The dedupe is not wrong to exist, it is wrong when its key is narrower than the thing it is deduplicating, and the narrower key is the one a reader reaches for first because `(kind, key_id)` is how every OTHER seam here addresses a pre-authorization. The asymmetry with the consuming seam is deliberate and must not be 'fixed' into agreement: `consume_redeemed` filters on `(kind, key_id)` WITHOUT the owner precisely because a redemption must make BOTH siblings due at once, while `armed_mutual`/`consume_mutual` serve the knock keyspace, where only the knocker's single record exists. INVARIANT: any replace-in-place keyed lookup states the FULL identity of the record it replaces; where a key is deliberately narrower than a record's identity, the seam says why and a test pins that two records sharing the narrow key both survive. Gate: doc -- the KNOWN-HAZARDS entry stating the sibling mechanism and the deliberate arm/consume asymmetry; impl -- the owner-inclusive match in the arming seam; unit -- two pre-authorizations sharing one code id under different owners both survive arming and are BOTH made due by one redemption, with a bystander code untouched and a replay finding nothing left.
2026-09-06T13:09:02.2536956Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2537121Z 
2026-09-06T13:09:02.2537226Z ### REQ-KNOCK-ANSWER-RECEIPT
2026-09-06T13:09:02.2543799Z - Title: A CROSS-NODE APPROVAL SENDS AN ANSWER RECEIPT BACK TO THE KNOCKER'S NODE, and that receipt is the only thing that consumes a knocker-local `--mutual` pre-authorization. Pre-auths are node-local BY DESIGN and stay so: nobody writes another endpoint's rules, so the knocker states its own conditional intention on its own side and its own daemon writes the reverse rule when the answer is proven. As shipped the only consumer runs inside the APPROVER's node against the APPROVER's store, so for every cross-node knock `armed_mutual` is None at the approver and the knocker's record dangles forever -- the seam CONTEXT.md's mutual-whitelisting entry already names ('written by their daemon at answer-receipt, keyed to the knock's correlation id and the node-proven answer') simply does not exist. THE RECEIPT IS ITS OWN KIND-TAGGED WIRE FAMILY MEMBER, on the `knockmsg`/`redeemmsg` precedent and for the identical reason: UNKNOWN FIELDS DECODE FINE, so a receipt smuggled as a field on an existing record would be ACCEPTED and MISREAD by an N-1 daemon, and misread is worse than dropped. A new kind resolves to the unknown family and drops clean. NODE-PROVEN OR IT CONSUMES NOTHING: the receipt's origin node is transport truth from the handshake identity, never a claimed field, and the consumed pre-auth is keyed to the knock's correlation id so a replayed receipt writes the reverse rule exactly once. AN UNPROVEN OR UNKNOWN RECEIPT IS NOT A REFUSAL AND NOT AN ANSWER -- it leaves the pre-authorization armed, because a conditional intention that silently expired reads identically to one that was never made. Gate: doc -- the entry naming the receipt family, its tag, and what a receipt is allowed to consume; impl -- the record, the send leg on the approve path, the classifier arm and the receiving consume seam; unit -- a receipt classifies into its own family and can NEVER be read as the message feed or as a knock (both asserted, since either alone leaves the other misclassification reachable), an N-1 receiver drops it clean, a proven receipt consumes the knocker-local pre-auth and writes the reverse rule ONCE while a REPLAY of the same receipt writes nothing further, a receipt whose correlation id matches no armed record writes nothing, and -- the discriminator, since 'consumed' and 'never armed' both end with no pending record -- an arrival that is NOT node-proven leaves the record ARMED rather than consumed, asserted beside the proven arrival on the same fixture so the two cannot both be satisfied by an implementation that ignores proof.
2026-09-06T13:09:02.2550693Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.2550863Z 
2026-09-06T13:09:02.2550979Z ### REQ-KNOCK-COUNTER-KNOCK-TRAVELS
2026-09-06T13:09:02.2558270Z - Title: RETIRED 2026-08-04 (ADR-0055, releases#144 bag grill) — ITS SUBJECT NO LONGER EXISTS. `knock approve --mutual` and the reciprocal knock it minted are REMOVED, not renamed: an approval carries no directionality, and a receiver who wants reverse reach knocks back through the knock verb itself. The routing bug this requirement fixed was real and its fix was correct for the shape that then existed; the shape is gone, so its impl, unit and int evidence deleted with the code. Retained in the registry rather than deleted so the reversal reads back. The superseded text follows verbatim. THE RECIPROCAL KNOCK AN APPROVAL MINTS RIDES THE SAME WIRE LEG A FIRST-ORDER KNOCK RIDES. `knock approve --mutual` mints a counter-knock addressed to the original knocker and, as shipped, upserts it into the APPROVER'S OWN local store -- so when the knocker is remote the approver's node holds a knock addressed to an endpoint that queries a different node and will never see it. It is not a delivery that failed; it is a delivery that was never attempted, silently, while the approver's stdout reads as though reciprocity was offered. The first-order path already calls `wansend::knock_send_remote` for exactly this reason and the counter-knock path does not; the paired notify is `NotifScope::Node` and by its own comment never enters a replication feed, so it carries nothing across either and STAYS as the design says for the remote case. THE ROUTE IS CHOSEN BY WHERE THE ORIGINAL KNOCKER LIVES, not by a flag: a local knocker keeps the local upsert (the shipped behaviour, which is correct for it), a remote knocker gets the send. AND THE APPROVAL ITSELF IS NEVER HELD HOSTAGE TO THE RECIPROCAL: the approval has already been decided and written when the counter-knock is minted, so a counter-knock that cannot be delivered reports its own failure and does not unwind or downgrade the approval it rode in on. Gate: impl -- the route split on the original knocker's node and the wansend leg for the remote arm; unit -- a REMOTE knocker's counter-knock leaves the approver's local store untouched and reaches the send leg, a LOCAL knocker's counter-knock still lands in the local store and does NOT reach the send leg (the pair asserted together, since either alone passes an implementation that routes everything one way), and a send-leg failure on the remote arm leaves the ALREADY-WRITTEN approval intact and still reported as approved.
2026-09-06T13:09:02.2564634Z - Required stages: 
2026-09-06T13:09:02.2564863Z 
2026-09-06T13:09:02.2565549Z ### REQ-KNOCK-REDEEM-MUTUAL-RECEIPT
2026-09-06T13:09:02.2572499Z - Title: VOCABULARY CORRECTED 2026-08-04 (ADR-0055, releases#144): the flag is --send-receive, its retired spelling was --mutual; the requirement itself is UNCHANGED and survives the ratification intact, since redeem is a REQUESTING verb and keeps its directionality declaration. `redeem --send-receive` ARMS THE REDEEMER'S OWN SIDE AND THE NODE-PROVEN `REDEEMED` REPLY CONSUMES IT. Unlike the knock case the receipt ALREADY EXISTS on this route -- the proven reply arrives back at the redeemer -- and is simply not wired to the flag, which is why this is materially cheaper than the answer-receipt build and not covered by it. ADR-0054's governing clause is the spec, verbatim: `--send-receive` stays redeemer-local, consumed once by the node-proven `REDEEMED` reply. As shipped the cross-node route arms NOTHING and refuses with a general rule -- that `--send-receive` arms the minter's side and is set at mint -- WHICH THE LOCAL ROUTE FALSIFIES by arming the redeemer's own side and consuming it in the same invocation: the refusal is keyed on the ROUTE but explains itself on the axis of flag ownership, so it is wrong wherever it is read. THE SUBJECT COMES FROM THE REPLY, NEVER FROM A LOCAL GUESS -- the redeemer holds no code record, so the counterpart identity and the surfaces are exactly what the proven reply carried, which is what the store's already-shipped empty-subject-means-whoever-answered resolution exists for. THE INTENTION IS RECORDED BEFORE THE OUTCOME IS KNOWN, so the three outcomes can be told apart on the record and not merely in a rendering: REDEEMED consumes, REFUSED disarms, and UNCONFIRMED NEITHER -- no proof, no open, and no false close either; the armed record survives the silence and expires with itself rather than being read as an answer. Gate: impl -- the arm on the redeemer's own store ahead of the send, keyed in the CODE keyspace, and the three-way consume/disarm/leave on the outcome; unit -- REDEEMED consumes exactly once and writes the reverse rule naming the subject THE REPLY carried (asserted against a reply whose target differs from anything the redeemer could have guessed locally, so a local-guess implementation fails the row), REFUSED leaves NO armed record, UNCONFIRMED leaves the record ARMED and unconsumed -- the three asserted on one fixture, since any two alone pass an implementation that collapses the third into a neighbour -- and a replayed REDEEMED reply writes the reverse rule no second time. Retirement row: the shipped cross-node refusal message is GONE from every surface that carried it, asserted by rendering the surfaces rather than by reading the source.
2026-09-06T13:09:02.2579955Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.2580161Z 
2026-09-06T13:09:02.2580438Z ### REQ-KNOCK-MINT-MUTUAL-IS-A-REQUEST
2026-09-06T13:09:02.2593342Z - Title: RETIRED 2026-08-04 (ADR-0055, releases#144 bag grill) — ITS SUBJECT NO LONGER EXISTS. `knock new-code --mutual` is removed: a mint declares no directionality at all, so there is no mint-side pre-authorization left to be a request or a grant. The finding this requirement recorded stands as history and its second property survives it — no surface may claim a reverse the claiming side did not open — which is now carried by the sender-declared model itself, where only the declarer's own act opens the declarer's own side. Its impl, unit and int evidence deleted with the code (including arms 2 and 3 of crates/spt/tests/knock_mutual_cross_node_e2e.rs; the surviving arms stay and remain REQ-KNOCK-REDEEM-MUTUAL-RECEIPT's). Retained in the registry rather than deleted so the reversal reads back. The superseded text follows verbatim. A MINT-SIDE `--mutual` IS A REQUEST FOR THE REVERSE, NEVER A GRANT OF IT, AND NOTHING MAY RENDER IT AS ONE. The reverse of a code grant is the TARGET reaching the REDEEMER, which can only be opened by a rule scoped on the REDEEMER'S OWN endpoint -- so no record held on the MINTING node can ever constitute that consent. As shipped, `new-code --mutual` armed a minter-local record whose consumption wrote scope=target subject=redeemer, which is the SAME DIRECTION the redemption grant already wrote: a DUPLICATE, not a reverse. The `REDEEMED` reply then carried a flag rendered to the presenter as `reach is TWO-way -- they may reach you as well`, asserting a direction that nothing had opened and that only the presenter's own act could open. TWO INDEPENDENT PROPERTIES, and the second is not implied by the first: the mint-side mutual must not DUPLICATE the forward grant, and no surface may CLAIM a reverse that the claiming side did not open. A repair that stopped the duplicate write while leaving the sentence standing would satisfy one and keep shipping the other. THE RATIFIED PRINCIPLES DECIDE THE INSTRUMENT and this requirement records which: `knock approve --mutual` was always the precedent -- when the side OFFERING reach wants it back, the instrument is a reciprocal KNOCK, not a grant -- so consuming the mint-armed record puts the target's knock in the redeemer's inbox, the redeemer answers it, and the reverse opens with the only consent that can open it. `full mutuality needs both` is FALSE under the grant spelling (`redeem --mutual` alone already writes the true reverse) and TRUE under this one, which is the tell that distinguishes them. WHICH SIDE A PRE-AUTHORIZATION IS MUST BE RECORDED AT ARM TIME, not re-derived at consumption by comparing an owner against the code's target: the two sides owe DIFFERENT things and a discriminant inferred at the check is a second source for what was known at the mint. THE REPLY IS THE CARRIER and the provenance is its own: the request rides the redemption reply already crossing the seam with node-proof in hand, and the knock the redeemer's node writes from it is stamped with a provenance distinct from a handshake-stamped first-order knock -- it is sound because the redeemer opened a sealed code with a subnet seed and received a node-proven reply in the SAME exchange, which is better evidence than an inbound receiver holds, and that argument does not generalize beyond this exchange. Gate: doc -- the CONTEXT.md mutual entry carrying the struck-and-amended mint-side clause; impl -- the arm-time side discriminant on the record, the consumption producing a reciprocal knock instead of a rule, the reply-carried request, and the distinct provenance at the write site; unit -- a mint-armed record's consumption yields a KNOCK and writes NO access rule (asserted against a fixture where the duplicate rule would be visible, since a repair that merely stopped rendering would pass a rendering-only row), a redeemer-armed record's consumption still yields its reverse RULE (the pair asserted together, since either alone passes an implementation that treats every record one way), the reply-written knock carries the distinct provenance and NOT the first-order one, and no surface renders a two-way claim on the strength of the minting side's record alone. Retirement row: the `they may reach you as well` sentence is gone from every surface, asserted by rendering.
2026-09-06T13:09:02.2611950Z - Required stages: 
2026-09-06T13:09:02.2612142Z 
2026-09-06T13:09:02.2612261Z ### REQ-VIS-REMOTE-NOT-HIDE-NEW
2026-09-06T13:09:02.2617597Z - Title: A REMOTE ENDPOINT'S VISIBILITY IS ITS OWN NODE'S BUSINESS, AND THE ASKING NODE'S `hide_new_endpoints` POSTURE GOVERNS ITS OWN ENDPOINTS ONLY. `VisibilityStore::hidden` resolves an explicit per-(endpoint, subnet) override first and otherwise falls through to `subnet_hide_new || default_hide`; a REMOTE id never has an override or a default-hide row in the ASKING node's local store, so on any node whose subnet has `hide_new_endpoints` ON that fall-through evaluates true for EVERY remote id and the resolve-side exclusion vetoes them all -- `resolve_across_visible` returns NotFound and every remote wan target is refused on that node. IT IS NOT CONSERVATIVE, IT IS BACKWARDS: `hide_new_endpoints` is the OWNING node's advertisement policy, captured at join and enforced at the advertisement/sync gate on the owning side, so a hidden remote endpoint has no registry row on the asking node at all. The asker-side re-application therefore gates nothing the owner has not already gated, and vetoes exactly the rows the owner deliberately advertised. THE OWNING-SIDE GATE IS UNTOUCHED -- this is a resolve-side scoping fix, not a relaxation of who advertises what -- AND EXPLICIT PER-ENDPOINT OVERRIDES KEEP WORKING IN BOTH DIRECTIONS, including on remote ids, because an operator who named a specific endpoint said something the default never said. Gate: impl -- the resolve-side default applying only to ids the local node owns, at one shared seam rather than re-decided per call site (the call-site count is grep-swept, not estimated); unit -- with `hide_new` ON, a remote id with no local rows RESOLVES while a LOCAL id with no local rows is still HIDDEN (the pair asserted together, since either alone passes an implementation that scopes everything one way), an explicit override still HIDES a remote id and still REVEALS a locally-owned one, and -- the vacuity guard, since a fixture whose rows are uniform in the asserted property proves nothing -- the same fixture with `hide_new` OFF is shown resolving both, so the ON-case difference is the posture answering and not the fixture.
2026-09-06T13:09:02.2623890Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.2624065Z 
2026-09-06T13:09:02.2624195Z ### REQ-KNOCK-HUMAN-PRESENTER-NOT-DEAD-ENDED
2026-09-06T13:09:02.2630114Z - Title: A HUMAN PRESENTING A VALID CODE REACHES A DECISION, RATHER THAN SILENCE THAT NEVER ENDS. A bare-terminal presenter binds the deliberately-narrower `node + origin user` subject -- the knock design's own human form, 'the humans on my node', ratified as narrower than a bare node entry. `RuleMutation::widens()` classifies widening by subject KIND ALONE, so the origin-USER qualifier does not narrow it, and redeeming ANY code whose mint did not carry `--admit-node` -- INCLUDING AN MSG-ONLY CODE, whose surface is perfectly attributable -- refuses the rule write, reaches no decision, and returns the two-silences serve arm. The silence is individually correct and collectively a dead end: nobody at MINT time knows whether a human will redeem, so the commonest invite there is -- here is a code, message me -- is a latent trap for exactly the least-technical presenter, and it starts at the mint, where the person who could have avoided it was never told. THE FIX SIDE IS A RULING, NOT A BUILDER'S CHOICE, and this requirement deliberately states the PROPERTY rather than the mechanism: whether `widens()` learns the qualifier axis (a subject admitting only the humans on a machine is multi-party but materially narrower, and the acknowledgment's own stated ground -- that it admits every endpoint on the knocker's node -- is false of it) or the redemption path carries a special case, the delivered property is the same. Weighed against it and to be answered by the ruling, not assumed away: humans on a machine can widen over time as users are added, and the grant-nodes policy may deliberately treat any node-scoped subject as node-tier whatever its qualifier. Gate: impl -- the ruled mechanism, single-sourced so the local verb and the wire serve side cannot answer differently; unit -- an MSG-only code minted WITHOUT the acknowledgment, presented by a bare terminal, reaches a DECISION and writes a rule that MATCHES the traffic it names (asserted against the real chain, not a second reading of it), presented by an ENDPOINT it still binds the sender-endpoint subject unchanged, and -- the non-vacuity row, since a mechanism that stops refusing everything would also satisfy the first -- a genuinely node-widening bake WITHOUT the acknowledgment is still REFUSED on the same fixture.
2026-09-06T13:09:02.2636813Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.2637147Z 
2026-09-06T13:09:02.2637337Z ### REQ-PSYCHE-INGEST-FAILURE-LOUD
2026-09-06T13:09:02.2653648Z - Title: A COMMUNE THAT CAN NEVER INGEST IS LOUD WHERE IT IS NOTICED, AND NEVER LETS A RESUME SERVE THE STALE TIER AS IF IT WERE FRESH. MECHANISM (field-settled, and NOT the one this requirement was minted describing -- see the amendment note): a commune written from a cwd other than the endpoint's REGISTERED one silently never enters the tier. The ingest resolves the manifest's drop dir against the endpoint's recorded cwd; a drop written from a worktree, or any non-registered dir, lands somewhere nothing watches. NOTHING FAILS -- so no error is stamped, no strike accrues, and the surfacing that does exist is structurally incapable of firing. The durable tier keeps the PREVIOUS commune and the next resume is rebuilt from it, confidently and wrong. THE SEAM IS AN ANCHOR DISAGREEMENT INSIDE ONE BRIEF: the ingest anchors the drop dir to the endpoint's registered cwd, while the CLI brief composer joined the raw manifest dir against nothing, binding it to whatever cwd the CALLER ran in -- so the durable tiers came from the endpoint and the pending-drop lookup came from the caller, and the two could name different directories without either being wrong on its own terms. TWO INVARIANTS, AND THE SECOND IS THE ONE THAT BITES: (1) a drop that can never ingest is surfaced at the instant a resume is composed over it -- the one moment the degraded party is listening -- and NAMES ITS ABSOLUTE PATH, because the recovery is reading the file and that is only actionable if the party is told where it is; (2) a resume composed over such a drop does not present its durable tiers as current -- the staleness is carried WITH the context rather than inferred by an agent lucky enough to have a nonce discipline. A LOST INGEST IS NOT A LOST FILE: the drop survives on disk and the wake leg fires correctly, so the recovery exists and is cheap. That is the field mitigation already proven to work, and naming the unspooled file is the actionable half of being loud. PAIR, NOT SEQUENCE (acceptance condition): before the anchor was corrected a worktree session found its own misplaced drop BY ACCIDENT, because the composer's lookup was wrong in the same direction the drop was; correcting the anchor without a deliberate misplacement arm in the SAME change would delete that accidental recovery and silently re-open the hole. SCOPE IS SPT-CORE-SIDE -- the daemon AND the CLI brief composer; the harness ADAPTER is untouched. That distinction is the whole of the scope line and is not to be re-litigated: `resolve_filedrops` is spt-core's own code at the seam the adapter calls, and leaving the CLI brief wrong fails invariant 2 at the exact surface an operator reads. Gate: impl -- one shared drop-dir resolver both composers route through, the misplacement arm paired with it, the compose-instant surfacing carrying the unspooled drop's absolute path, the resume refusing to present a never-ingested drop's tiers as fresh, and the registered drop dir surfaced on the endpoint's own info (the only fact that makes a surviving drop classifiable as misplaced-versus-failed by anyone but its owner); unit -- a misplaced drop emits its surfacing ONCE and names the file while a CONSUMED drop emits none (the pair asserted together, since the misplaced row alone passes an implementation that shouts on every compose), a resume over a misplaced drop is marked stale while the SAME resume over a registered drop is not (the discriminator, since both otherwise return the same shaped context and an implementation that marks nothing, or everything, satisfies either row alone), and -- the acceptance row -- a misplaced drop is still FOUND after the anchor is correct, since the anchor fix alone would make it invisible and read as a clean pass.
2026-09-06T13:09:02.2668836Z - Required stages: impl, unit, int
2026-09-06T13:09:02.2669139Z 
2026-09-06T13:09:02.2669281Z ### REQ-PSYCHE-OUTCOME-CLASSIFIED
2026-09-06T13:09:02.2673928Z - Title: A PSYCHE OPERATION'S OUTCOME IS CLASSIFIED AT ITS SOURCE AND CARRIED AS A TYPE, NEVER RECOVERED BY READING ITS PROSE. The soft-timeout budget and the hard-failure budget can only differ if the two are told apart, and today they cannot be: `RuntimeError::Timeout { command, after }` exists at the source (spt-runtime runtime.rs) and `EchoError::Runtime` wraps it, but `fire_echo` ends `.map_err(|e| e.to_string())?` and `note_turn_outcome` takes `&Result<(), String>` -- so by the time the budget sees an outcome the fact is prose. RECOVERING IT BY MATCHING THE MESSAGE IS FORBIDDEN, and not as a matter of taste: this codebase already ruled that class out for the sibling discriminator, where the reserved exit code 95 is the SOLE session-not-found signal and the comment at its site states that the human-readable stderr line is never parsed. A harness whose summarizer prints the words `timed out` inside an ordinary non-zero failure must not thereby buy the softer budget. THE CLASSIFICATION IS THREE-VALUED (Ok / Timeout / Hard) and both legs -- the per-event turn and the commune-sync echo -- produce it, so the budget owner never re-derives what the spawn already knew. Gate: impl -- a classified outcome type produced at the runtime boundary from `RuntimeError::Timeout` and threaded unflattened through `fire_echo` and the turn leg into `note_turn_outcome`, which consumes the type and never the string; unit -- a HARD failure whose stderr text CONTAINS `timed out` still classifies Hard (the anti-parse row: a message-matching implementation passes every other row and fails this one), a real bound kill classifies Timeout, and a clean run classifies Ok.
2026-09-06T13:09:02.2678486Z - Required stages: impl, unit
2026-09-06T13:09:02.2678645Z 
2026-09-06T13:09:02.2678758Z ### REQ-PSYCHE-INVOCATION-BUDGET-PER-ROLE
2026-09-06T13:09:02.2684178Z - Title: EVERY BOUNDED ADAPTER INVOCATION TAKES ITS KILL BOUND FROM THE ROLE THAT DECLARES IT, WITH A SANE DEFAULT AND A HARD CEILING. FIELD ROOT (releases#27, two independent confirmations the night of 2026-07-29): the echo-commune summarizer ran against a flat 30s bound while the legitimate turn measured 23s and 35s wall on the same node with the same input -- the honest duration STRADDLES the bound, so kills were a load-of-the-moment lottery, and three agents were struck in one night. A LOTTERY IS NOT A FAULT SIGNAL. THE KNOB IS GENERAL, NOT ECHO-SPECIFIC: any role that is an LLM turn declares its own budget on the manifest role, because the adapter is the only party that knows what its own model costs. Numbers ruled: 90s when undeclared, and core CLAMPS a declared value to a 300s ceiling -- an adapter may not buy an unbounded spawn by declaring one. NO ROLE-NAME SPECIAL-CASING: the resolution keys on the declared FIELD, never on which role it is, matching the manifest contract's existing discipline for the recursion-guard field. THREE BOUNDS COLLAPSE INTO THIS ONE, verified by sweep rather than assumed: `LIVE_BOUNDED_TIMEOUT` governs BOTH the summarizer spawn and the history fetch, and `event_turn_timeout_for` separately governs the psyche_resume per-event turn on a pulse-derived value with a 30s floor. Ruled consequences, deliberate and not side effects: the history fetch moves 30s -> 90s undeclared, and the pulse-derived turn bound is REPLACED by the role budget (measured before replacing: default pulse 5s yields 30s today, under the new default, so nothing fielded is shortened). Gate: impl -- the budget field on the manifest session role, one resolution seam applied at EVERY bounded spawn site, the 300s clamp, and the pulse-derived turn bound retired in favour of it; unit -- undeclared resolves 90s, a declared value under the ceiling resolves verbatim, a declared value over it resolves 300s, and -- the no-special-casing row, since every other row passes an implementation that hardcodes the echo role -- a role the resolver was never taught by name resolves its declared budget identically.
2026-09-06T13:09:02.2689791Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2689957Z 
2026-09-06T13:09:02.2690056Z ### REQ-PSYCHE-SOFT-TIMEOUT-BUDGET
2026-09-06T13:09:02.2693961Z - Title: A TIMEOUT KILL IS A LOAD SIGNAL, NOT A DEFECT, AND SPENDS A SEPARATE AND FAR MORE FORGIVING BUDGET. A slow-but-healthy host must never latch FAILED: three consecutive bound kills during a CI load window are exactly what the field produced, and under the shipped single 3-strike budget they stamped healthy psyches. THE COUNTER IS ONE, SHARED ACROSS KINDS, and latches only after TEN consecutive timeout kills. ANY SUCCESS OF ANY KIND CLEARS IT, including a success of a different kind than the one timing out -- a timeout is evidence about the machine's load at an instant, and any completed spawn is evidence that the window passed, so cross-kind clearing is the semantics rather than a convenience. THAT CROSS-KIND CLEAR IS WHAT DISSOLVES THE ORIGINAL LATCHED-STAMP FIELD CASE, whose latches were timeout-driven. A timeout NEVER touches a hard-failure counter, in either direction: it does not strike one and it does not reset one. Gate: impl -- the shared consecutive-timeout counter, its ten-deep latch, its clear on any success of any kind, and its isolation from the hard counters; unit -- nine consecutive timeouts leave the stamp clean and the tenth stamps (the boundary asserted from both sides, since a row at one depth alone passes an off-by-one), a success of a DIFFERENT kind clears the timeout counter (the ruled cross-kind semantics, which a per-kind implementation fails while passing every same-kind row), and a run of timeouts leaves an existing hard counter at its prior value rather than resetting it.
2026-09-06T13:09:02.2698068Z - Required stages: impl, unit, int
2026-09-06T13:09:02.2698225Z 
2026-09-06T13:09:02.2698338Z ### REQ-PSYCHE-HARD-BUDGET-PER-KIND
2026-09-06T13:09:02.2705170Z - Title: INGEST JOINS BOTH SIDES OF THE PSYCHE HEALTH STAMP, AND HARD FAILURES ARE COUNTED PER KIND SO ONE WORKING SUBSYSTEM CANNOT VOUCH FOR A BROKEN ONE. FILED ROOT (releases#97): clearing is keyed to psyche TURNS while the health question in the field was drop INGEST, so an endpoint printed FAILED with a stale stamp while ingest demonstrably worked. THE FAILING SIDE IS WORSE THAN FILED, found at recon: the ingest leg is `ingest_drops(...)?` inside `pulse_tick` and the loop swallows the error (`Err(_) => { /* a transient tick error must not kill the loop */ }`), so a hard ingest failure today produces NO strike, NO stamp and NO stderr line, and additionally SKIPS that tick's psyche turn and commune-sync entirely. FIX, RULED: capture-and-continue -- the ingest outcome is classified and fed to the budget BEFORE the turn runs, and the tick then proceeds to the turn and echo. The behaviour change is deliberate: a turn over possibly-stale context, plus a struck budget, plus the expectation surfacing, beats a silently dead psyche forever. AN INGEST SUCCESS IS AT LEAST ONE DROP ACTUALLY INGESTED -- an empty sweep is neither success nor failure, because nothing ran and nothing was proven healthy, the same reasoning the role-absent skip already carries. THAT DEFINITION IS LOAD-BEARING, NOT PEDANTRY: were an empty sweep a success, the stamp would clear on nearly every tick, no budget could ever reach its latch, and the change would read as a fix while disabling the stamp it repairs. COUNTERS ARE PER KIND (turn, ingest), three consecutive each, and a success resets ONLY its own kind -- ingest working does not disprove a turn defect, nor the reverse. The stamp remains the single existing `psyche_host_error` field, current-state, and its reason names the kind and the class. Gate: impl -- the ingest error captured rather than propagated, classified, recorded before the turn, the tick continuing to turn and echo, per-kind consecutive hard counters with own-kind-only reset, and the reason text naming kind and class; unit -- three consecutive ingest hard failures stamp with an ingest-hard reason WHILE turns are succeeding on the same fixture (the row the shipped code cannot pass and a shared-counter implementation also fails, since the turn successes would reset it), an ingest success clears the ingest counter and leaves a standing turn counter untouched, an EMPTY sweep neither clears nor strikes either counter (asserted against a fixture with a non-zero counter standing, since over a zeroed counter a clearing implementation and a correct one agree), and a hard ingest failure still lets the SAME tick run its turn (the capture-and-continue row, which the pre-fix `?` fails by construction).
2026-09-06T13:09:02.2712553Z - Required stages: impl, unit
2026-09-06T13:09:02.2712700Z 
2026-09-06T13:09:02.2712810Z ### REQ-PSYCHE-FAULT-SLOTS-SURVIVE-RESTART
2026-09-06T13:09:02.2721803Z - Title: A PSYCHE FAULT STAMP REMEMBERS WHICH LATCH SLOT PRODUCED IT, SO A DAEMON RESTART CANNOT CLEAR A FAULT IT NEVER SAW. ROOT (releases#115): the three fault latches -- latch_turn, latch_ingest, latch_timeout -- live in memory behind a Mutex and are never seeded from disk, while the `psyche_host_error` stamp they compose to OUTLIVES the process on disk. After a bounce the stamp stands with nothing standing behind it, so the first `note_success` of ANY kind recomposes an empty latch set to None and clears a fault whose kind this process never learned. THE PER-KIND SEPARATION BAROMETER W1 BUILT IS THEREFORE UNDONE BY A RESTART: one working ingest erases a standing turn defect it says nothing about, which is precisely the vouching REQ-PSYCHE-HARD-BUDGET-PER-KIND exists to forbid -- the defect does not lose information at the margin, it re-opens a closed hazard. FIX: the stamp carries a SLOT-KEYED sibling map written by the SAME seam that writes the reason, so the sentence and the latches that produced it can never disagree on disk, and the pulse loop seeds its latches from that map at startup BEFORE any tick can report a success. THE KEY IS A LATCH SLOT, NOT A KIND, and the distinction is load-bearing rather than nominal: there are THREE slots against TWO kinds, because the timeout latch is shared cross-kind by the ruled BAROMETER W1 semantics and belongs to no kind at all -- a kind-keyed reading would be re-read later as a per-kind register and gain a fourth key the day a fourth kind appears. THE VOCABULARY IS CLOSED to those three slots, and an unrecognized key is IGNORED AND ANNOUNCED rather than seeded: an unknown slot can only come from a newer binary owning a latch this one has no code to clear, so seeding it would strand a fault that no success could ever recompose away, while dropping it silently would make a rollback that loses a standing fault look identical to a recovery. A PRE-#115 STAMP CARRIES NO SLOTS AND SEEDS NOTHING, deliberately: its kind is genuinely unrecoverable, and the honest behaviour for an unknown-kind fault is the one that shipped, where any success clears it (REQ-PSYCHE-STAMP-CLEAR-ANY-SUCCESS) -- inventing a kind for it would be worse than forgetting one. `reason` stays BYTE-IDENTICAL, and the map is serde-default plus skip-when-empty so a stamp with no standing latches serializes byte-identically to the pre-#115 shape; N-1 holds in BOTH directions, the old-binary/new-record leg proven by a MEASURED absence of any live deny_unknown_fields attribute repo-wide (zero, on this struct and on InfoJson alike) rather than by assuming serde's default. Gate: impl -- the slot-keyed map on the stamp, written by the single existing writer alongside the reason it composed to, the closed three-slot vocabulary with ignore-and-log on an unknown key, and the pulse-loop seed running before the first tick; unit -- a stamp whose slots record a standing INGEST fault SURVIVES a fresh process reporting a TURN success (the row the shipped code cannot pass, since an unseeded set recomposes to None and clears it), a seeded latch IS still cleared by a success of ITS OWN slot (asserted alongside, since the survival row alone passes an implementation that has simply stopped clearing anything), a pre-#115 SLOTLESS stamp is still cleared by any success (the N-1 behaviour-preservation row), and an UNKNOWN slot key seeds NO latch while a known key beside it in the same map DOES seed (the pair asserted together, since over an unknown key alone a seed-nothing implementation and a correct one agree).
2026-09-06T13:09:02.2730944Z - Required stages: impl, unit
2026-09-06T13:09:02.2731087Z 
2026-09-06T13:09:02.2731197Z ### REQ-PSYCHE-INGEST-INTENT-MARKER
2026-09-06T13:09:02.2737261Z - Title: A COMMUNE THAT WAS EXPECTED AND NEVER ARRIVED IS DETECTABLE POSITIVELY, PER ATTEMPT, RATHER THAN INFERRED FROM AN ABSENCE. ROOT (releases#96, split out of the #90 recon): the summarizer spawn killed at its bound dies BEFORE writing the drop file, so it produces no file and no ingest -- and every presence-based check is structurally blind to it, because there is nothing on disk to find and nothing failed loudly enough to stamp. AN ABSENCE CANNOT BE OBSERVED; AN EXPECTATION CAN. THE DISCRIMINATOR IS A WRITE-AHEAD INGEST-INTENT MARKER: the lifecycle persists an expected-ingest record (timestamp + trigger) BEFORE launching the summarizer spawn, a completed ingest CONSUMES it, and a marker still standing past the spawn budget with no corresponding drop positively identifies the killed-at-bound case for that attempt -- no staleness threshold to tune and no guessing. THE ORDERING IS THE PROPERTY, not an implementation detail: a marker written after the spawn returns cannot survive the case it exists to detect, exactly as an intention recorded only after an answer arrives cannot survive an answer that never comes. SURFACING rides where the degraded party is already listening -- at resume/compose, beside the un-ingested-drop warning, saying that a commune was expected and never ingested and that the reader may be resuming stale. The marker is therefore written by the daemon lifecycle and read by the CLI compose path, so its storage must serve both. A killed-at-bound attempt counts on the SOFT timeout budget, never the hard one. Gate: impl -- the marker persisted before the spawn with its timestamp and trigger, consumed on ingest completion, the orphan classified past the budget, the resume/compose surfacing, and the storage readable from the CLI compose seam; unit -- a spawn killed at its bound leaves a standing marker with no drop and classifies as the expected-never-ingested case, a completed ingest leaves NO marker (the pair asserted together, since the orphan row alone passes an implementation that never consumes and would then report every healthy commune as orphaned), a marker whose drop IS present is not classified orphaned, and the write-ahead ORDERING is asserted directly by observing the marker from a spawn that never returns -- a post-spawn write passes every other row and fails this one.
2026-09-06T13:09:02.2743484Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2743637Z 
2026-09-06T13:09:02.2743751Z ### REQ-SHELL-PERSISTENT-BOOT-RESTORE
2026-09-06T13:09:02.2754116Z - Title: A `persistent` SHELL COMES BACK AFTER A NODE RESTART, WHICH IS THE ONE CASE THE CONTRACT'S OWN SENTENCE PROMISES AND THE IMPLEMENTATION NEVER HANDLED. CONTEXT.md states it plainly -- a persistent shell is automatically brought online whenever its owner endpoint is online -- and the shipped code honors that across a graceful suspend->wake rest edge ONLY. A machine restart produces no such edge and there is no boot-time restoration at all, so after a reboot every persistent shell on the node stays down until a human runs `relink` by hand, with no surface saying so. Field, HFENDULEAM 2026-08-01: the fleet came up at 01:11 and NOT ONE persistent shell came back; the two instances that did were relaunched by a human at 01:27, sixteen minutes later. TWO INDEPENDENT GATES, both of which must fall or the promise stays broken. GATE 1, no restoration path: the five launch sites all require either an explicit command, a rest edge, or a `wake_command` the adapter may deliberately not declare, and daemon boot starts only the servicehost sweep and the wake reconciler. GATE 2, the stale record blocks the cascade even when an edge DOES arrive: the to_active arm gates on the RECORDED status being offline, and a machine death breaks no link, so the close that would write offline never runs and the record says online over a corpse forever. LEG (a) HEALS THE RECORD TO THE TRUTH, with a BIRTH-STAMPED probe rather than bare pid liveness -- a pid namespace resets at boot, so a corpse pid can be REUSED by an unrelated process, and a reused pid reads alive, makes the derived status read online, and makes `relink` refuse SHELL_ALREADY_ONLINE: the one command that recovers the instance refusing on the very state that is wrong. TWO STAMPS ARE PARKED, NOT ONE, AND THE REASON IS A PLATFORM ASYMMETRY THAT WOULD OTHERWISE SHIP SILENTLY BROKEN ON LINUX: `process_started_at` returns a Windows FILETIME (absolute, comparable across boots) but on Linux returns field 22 of /proc/<pid>/stat, which is JIFFIES SINCE BOOT and resets every boot, so a value recorded before a restart is indistinguishable from a fresh one after it. The NATIVE stamp is therefore used for one question only -- the same-pid-different-process PAIR test within a single boot, mirroring `liveness::relay_liveness` -- and an ABSOLUTE epoch-ms launch stamp is parked beside it for the cross-boot question. LEG (b) IS A ONCE-PER-DAEMON-GENERATION BOOT SWEEP relaunching persistent instances whose owners are online, and its discriminant is THE LAUNCH STAMP PREDATES THE BOOT INSTANT, which preserves the class-c force-kill ruling BY CONSTRUCTION rather than by a second rule: a force-kill during steady-state operation leaves a corpse launched AFTER boot, so it is never spontaneously relaunched and the mid-deploy exe-overwrite hazard that ruling protects is untouched. A NAMED SLACK CONSTANT absorbs boot-instant jitter so a process launched moments after boot is never misclassified as a corpse. Gate: impl -- the two stamps parked at the two production pid-write sites (the census is authoritative: five LAUNCH sites funnel through exactly TWO pid writes), the birth-stamped death probe, the record heal in the daemon-side reconciler guarded on an actual change of the recorded value, the boot-instant helper on both platforms, and the boot sweep with its predates-boot discriminant plus slack; unit -- a reused pid (same pid, different birth stamp) reads GONE where bare-pid liveness reads alive (the row the shipped probe cannot pass), a same-pid-same-birth process reads HELD, the heal writes offline over a stale online record and writes NOTHING when the record already matches the truth (the guard asserted by counting writes, since a heal that rewrites every cycle passes any state-only assertion), a corpse launched BEFORE the boot instant is eligible for restore while one launched AFTER it is NOT (the class-c pair, asserted together since either alone passes an implementation that restores everything or nothing), and an instance whose OWNER is offline is not restored even though its corpse predates boot.
2026-09-06T13:09:02.2764754Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2764909Z 
2026-09-06T13:09:02.2765015Z ### REQ-SHELL-OWNER-ONLINE-RESTORE
2026-09-06T13:09:02.2773203Z - Title: A `persistent` SHELL WHOSE OWNER ENDPOINT WAS NOT YET ONLINE WHEN THE BOOT SWEEP RAN IS RESTORED WHEN THAT OWNER COMES ONLINE, RATHER THAN STAYING DOWN UNTIL A HUMAN RELINKS IT. ROOT (releases#228): daemons boot before endpoints do, so the boot sweep of REQ-SHELL-PERSISTENT-BOOT-RESTORE correctly SKIPS an owner that is not up yet -- an offline owner is owed nothing -- and then nothing ever revisits it: the sweep is once per daemon generation, and bringup from offline produces no rest edge, so the ADR-0048 cascade never fires either. The shell stays down until a `shell cmd` wakes it or an operator relinks by hand, and whether a given node restart strands its shells is an ordering accident. THE ONCE-NESS IS SCHEDULING, NOT SAFETY (doyle, releases#228, re-measured at head): the `launch_predates_boot` corpse-boot discriminant is what preserves the force-kill ruling, and it is MONOTONE WITHIN A MACHINE BOOT -- every launch path restamps `launched_ms`, a kill never restamps, `boot_ms` is the machine boot instant -- so re-evaluating the conjuncts later in a generation can only refuse MORE, never less. FIX: the reconcile loop carries the SAME per-owner restore body the boot sweep runs, triggered on owner offline->online transitions since the last pass. THE TRIGGER IS AN EDGE, NOT A LEVEL, and the distinction is the whole bound: success self-limits because the launch restamps the instance out of eligibility, but a FAILING launch under a level trigger -- owner is online -- retries every five seconds for as long as the owner stays up, while an edge bounds it to one attempt per owner-online event. THE REFUSAL ARMS ARE CARRIED BY CONSTRUCTION, not by restatement: both triggers call one shared per-owner body, so persistent-only, owner online, down in fact via the derived read with the pair test, stamp present, launch predates boot, and no boot oracle means restore nothing can never drift apart between the two paths. THE EDGE STATE IS SEEDED BEFORE THE SWEEP, deliberately: seeded after, an owner that comes online in the window between the sweep and the first tick reads as already-online and never fires -- the exact miss this requirement exists to close -- while seeding before can at worst re-attempt an instance the sweep already restored, which the RESTAMPED BIRTH STAMP refuses -- and NOT the down-in-fact arm, which is the arm that reads like the answer and is not: a launch is not the online switch, so a just-restored instance keeps an `offline` record until its bind-shell handshake and still reads as down in fact; what refuses the second attempt is that the launch moved `launched_ms` past the boot instant. Each trigger names ITSELF in its event line, because the field evidence that diagnosed this ticket was a count of those events and a shared name would leave the next investigation unable to say which trigger fired. Kin REQ-SHELL-PERSISTENT-BOOT-RESTORE (the boot trigger and the shared arms), REQ-HAZARD-RESTART-STRANDS-PERSISTENT-SHELLS (the invariant this closes the remaining seam of), REQ-SHELL-2 (the online/offline mutual exclusivity the edge path holds by stopping an armed watcher before it launches -- mid-generation a watcher CAN be armed for the casualty, unlike at boot).
2026-09-06T13:09:02.2781753Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2781910Z 
2026-09-06T13:09:02.2782011Z ### REQ-SHELL-KILL-AUTHENTICATED
2026-09-06T13:09:02.2795918Z - Title: A RECORDED PID IS NOT KILL AUTHORITY ON THE PRODUCT PATH EITHER: no shell-lifecycle kill fires until the pid+birth PAIR proves the process at that pid is still the one we launched. BAROMETER W2 authenticated the DERIVATION path (`shellinfo::shell_pid_provably_dead` runs the pair test) and left the KILL path believing a bare pid, so the same recycled-pid state that the derived read correctly calls a corpse still routes to `taskkill /PID <pid> /T /F` (unix `kill -9`) against whatever unrelated process now wears that number -- and `/T` takes its whole subtree with it, on the user's machine. Proven reachable by probe, not inferred: a live stranger's pid parked beside a foreign birth stamp is killed by `kill_shell_at` while `shell_pid_provably_dead` calls the same pair a corpse. SITE SHAPE IS TWO CHOKEPOINTS, NOT THREE CALL SITES: `shellhost::kill_shell_at` (reached by `close_shell` and the `spt shell teardown` force path) and `shellwake::kill_waker_at` (reached by `launch_shell`, `close_shell`, `stop_watcher`, and the reconciler), both funnelling into the single `kill_shell_pid` taskkill; the gate therefore lands in the two chokepoints and NO caller changes. THE WAKER HALF NEEDED A WRITE-SIDE MINT BEFORE ANY GATE COULD MEAN ANYTHING: `waker.pid` was written bare, and `relay_liveness(live_pid, None)` is HELD by design (pre-stamp records fail toward alive), so a read-side gate there would have returned Held for every record that exists today and every one written tomorrow -- a VACUOUS gate that proves green. The pair is therefore minted in the waker's single existing write as a two-line `waker.pid` (`<pid>\n<birth>`): ONE file, ONE write, so the pair is atomic -- a second file has a torn window in which the pid is on disk and the stamp is not, and that window reads Held, which is the vacuous arm reintroduced as a race. Write and every reader move through ONE shared record/read function pair, mirroring `record_shell_launch`/`read_shell_launch`. FAIL-TOWARD-ALIVE IS THE PARSE ARM TOO: an unparseable record reads None and REFUSES the kill while LEAVING the record, because kill-nothing-and-retire-the-pid-file-anyway orphans a live waker while presenting success -- worse than the defect being fixed. UNPROVEN REFUSES: the destructive arm treats unknown as do-not-fire (consequence asymmetry -- a mis-fire force-kills a stranger's tree, a missed kill leaves our own binary running and observable), which FALSIFIES the shipped unconditional promise that a link-break's force-close always kills and "the binary never survives", so `close_shell`'s step 3, the REQ-SHELL-2 language, the CONTEXT lifecycle line and KNOWN-HAZARDS 7.58 are amended together with the code rather than one comment patched. THE REFUSAL IS LOUD: every declined kill names the reason and the pid on the channel the close path already speaks, and the bounded confirm loop reports a binary that outlived it -- a quiet wrong state is the un-RCA-able one, and the harm-asymmetry argument has to hold in the log and not only in principle. THIS NARROWS THE MIS-FIRE, IT DOES NOT ELIMINATE IT, AND THE TEXT SAYS SO: on Linux `process_started_at` reads /proc/<pid>/stat field 22, which counts CLK_TCK=100 jiffies, so two processes whose starts fall in the SAME 10ms tick carry an identical stamp and the pair cannot separate them. The window closes from ANY recycled pid to A PID RECYCLED INTO THE SAME 10ms TICK AS THE RECORDED START -- which requires our process to die AND the OS to hand its number to a new process inside one tick, a precondition that is its own mitigation. Windows is unaffected (FILETIME, far finer resolution). Measured by hertz on kitsubito; recorded verbatim so nobody re-derives it. THE ASYMMETRY IS WHY THIS IS THE KILL PATH'S SENTENCE TO WRITE: the same predicate with the same imprecision is SAFE on the derivation side and UNSAFE here -- Held means 'stays stale' for a derived read and 'fire the kill' for a destructive one, so only the kill side carries the residual harm and only the kill side has to declare it. Image-path corroboration (which would unify this rule with the rig-side technique in KNOWN-HAZARDS 7.58) is filed past as releases#120, deliberately not taken in a closing milestone. MIGRATION WINDOW, NAMED NOT SILENT: pre-W2 shell records and pre-fix waker records carry no stamp, read Held on existence alone, and keep today's bare-pid kill until their next launch writes a pair. DOWNGRADE DIRECTION, NAMED: an OLD binary reading the NEW two-line `waker.pid` parse-fails to None, which reads as no-pid and refuses the kill -- missed-kill, not mis-fire, the safe polarity. Gate: impl -- the pair gate in both chokepoints, the shared waker record/read pair with the two-line format, every waker.pid reader moved onto it, the loud refusal arms, and the confirm-loop survivor report; unit -- a recycled pid (live, birth mismatch) is NOT killed where the shipped code kills it, a genuine own-process pair (Held) IS still killed so the gate is not vacuous, an absent stamp keeps the pre-stamp kill, Unproven refuses, an unparseable waker record refuses AND leaves the record in place (asserted together, since retire-anyway passes a kill-count assertion alone), and the refusal is observable rather than silent; int -- hertz's probe inverted: a live stranger parked beside a foreign stamp SURVIVES `kill_shell_at` while `shell_pid_provably_dead` still calls the pair a corpse (the control), with the victim checked through `try_wait` on the held handle rather than a pid-existence probe, which reads alive for an already-killed process while the handle is open.
2026-09-06T13:09:02.2810218Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.2810384Z 
2026-09-06T13:09:02.2810524Z ### REQ-HAZARD-RESTART-STRANDS-PERSISTENT-SHELLS
2026-09-06T13:09:02.2816618Z - Title: A NODE RESTART MUST NOT PERMANENTLY STRAND EVERY `persistent` SHELL, AND THE EXISTING CASCADE TEST CANNOT SEE THAT IT DOES. Distinct from REQ-HAZARD-SHELL-STALE-ONLINE, which governs a force-kill DURING operation: this is a different trigger with a different safe remedy, and conflating them is how the restart class stays invisible. THE COVERAGE GAP IS THE HAZARD'S OWN SUBJECT, and it must be named here so no future reader mistakes the cascade test for coverage of this class: `rest_edges_cascade_shells_with_divergence` (spt-daemon resting.rs) is the cascade's only coverage and it walks the graceful path ONLY -- it SUSPENDS FIRST, and the suspend edge WRITES the offline record, so by the time the wake edge fires the instance is already in exactly the state the to_active arm requires. The restart shape -- record left online over a corpse, no rest edge ever -- is UNREACHABLE FROM THAT TEST BY CONSTRUCTION. The test is not weak; its SETUP SUPPRESSES THE FAILING ARM, so a reader asking whether the cascade is covered gets a true answer to the wrong question. This is the third suppressed-arm instance found in one week, and the pattern is the same each time: a fixture that establishes the precondition the code under test is supposed to establish. THE OWNER-FACING SURFACES HIDE THE FAULT, which is why it survived unreported for days: every display path (`shell list`, its --json twin, the shell-context render) routes through the ONE discovery seam, which DERIVES status and so correctly reports the binary as offline, while the only consumer whose decision matters -- the wake cascade -- reads the RECORDED field that no owner-facing surface shows. Absence of a visible symptom is therefore not evidence of absence of this fault, and a clean-looking context render must never be taken as proof the class did not occur; the correct falsifier is the on-disk record. Gate: impl -- the restart shape reaches a restoration path at all (leg b) and the record stops lying (leg a); unit -- the RESTART shape specifically, constructed WITHOUT a suspend edge: an instance whose record says online over a corpse that predates boot, with an online owner, is restored -- the row `rest_edges_cascade_shells_with_divergence` cannot express, asserted on a fixture that never calls the suspend path, since a fixture that suspends first re-creates the suppressed arm and proves nothing about this class.
2026-09-06T13:09:02.2823083Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2823245Z 
2026-09-06T13:09:02.2823345Z ### REQ-FIXTURE-BIN-BUILD-EDGE
2026-09-06T13:09:02.2838037Z - Title: IR-21 remedy (c), ENFORCED FORM: every test consumer of a fixture `[[bin]]` is CLASSIFIED by whether that bin's build is guaranteed, by a check anyone gating can run, rather than by whoever remembers the rule. THE MECHANISM, measured 2026-08-03 and NOT the one the class was originally filed on: cargo builds EVERY bin target of a package whenever it builds ANY integration test of that package, so a SAME-PACKAGE INTEGRATION consumer is already guaranteed and `env!("CARGO_BIN_EXE_*")` adds NO edge there -- the first remedy claim to the contrary was measured FALSE and the register entry was corrected. The real exposure is CROSS-PACKAGE consumers and SAME-PACKAGE UNIT tests, which get no `CARGO_BIN_EXE_*` at all; that unit member (`crates/spt/src/cli.rs`, resolving `format!("translate_proof_fixture{EXE_SUFFIX}")` relative to `current_exe()`) is exactly why `.github/workflows/ci.yml:105-106` hand-prebuilds that fixture. THE CHECK READS THE DEPENDENCY SITUATION, NOT THE EXPRESSION -- owning package, consuming package, target kind -- so it classifies an UNCONVERTED tree correctly and stays true after any consumer is rewritten; that property is what makes it adoptable independently of the sibling_bin->CARGO_BIN_EXE_ conversion lane. FOUR SYNTAXES NAME A FIXTURE BIN and all four are detected: `sibling_bin("n")`, `CARGO_BIN_EXE_n`, the shared resolver `fixture_bin("n", "cargo build ...")` (crates/spt-term/tests/support/fixture_bin.rs), and the `current_exe()`-relative `format!("n{...}")`. A TWO-SYNTAX DETECTOR IS THE FAILURE MODE THIS REQUIREMENT EXISTS OVER: the first prototype knew two, missed seven sites including that unit member, and therefore returned a GREEN over the single gap the class was filed for -- a zero from a tool that could not see the known-real case. Population at 6d291e0: 185 consumer sites, 146 guaranteed, 39 needing an explicit build, 11 with no prebuild anywhere. THE 11 ARE NOT CI DEFECTS: a per-job mapping showed every narrow cargo invocation in CI is covered by its own job (prebuild or an earlier workspace-shaped build), so the exposure is narrow invocations OUTSIDE CI -- gate rigs and local `-p`/`--test`/`--bins` against a cold pool -- which is why the check ships as a runnable command and is deliberately NOT wired into `xtask check`: it exits 1 on today's tree and wiring it would red the docs gate over a hazard that is not a CI defect -- a gate that reds on a population of known non-defects gets ignored or gets its teeth pulled, and either way the next REAL member arrives into a check nobody reads (doyle ruling, 2026-08-03). THE WIRING CONDITION IS THEREFORE A COMMITTED BURN-DOWN BASELINE, NOT FIXING ALL 39 FIRST, and it ships here so the choice to wire is a one-line decision later rather than a second design problem: `--baseline` reds ONLY on a site absent from `crates/xtask/binedge-baseline.tsv`, exits 0 against today's recorded 11, and REPORTS recorded entries that are gone so the list shrinks instead of ossifying into a permitted-forever set nobody prunes. Baseline keys are `(file, bin)` and deliberately NOT line numbers, which churn on every edit above them and would make the file merge noise; the trade is named -- a SECOND red site for the same bin in an already-listed file is absorbed. TWO CONTROLS SHIP WITH IT RATHER THAN LIVING IN A RUNBOOK. `--no-prebuilds` empties the CI allowlist and must take the reds 11 -> 39 while the 146 cargo-guaranteed sites stay green; a green without that arm carries no information. The false-positive arm asserts the same-package fixture sites are classified guaranteed -- and REFUSES (exit 2) when it recognizes NONE of them, because an arm whose fixtures were renamed away reports `0 wrongly flagged`, which reads exactly like a pass. That arm also filters on target kind, not name alone: it was briefly wrong when a same-package UNIT consumer entered the population and CORRECTLY landed in the flagged set, so a control is a claim about a population and ages with one. Gate: impl -- `crates/xtask/src/binedge.rs` and its `binedge-check` subcommand: the four-syntax scanner over comment-blanked whole text (line comments only; block comments are a NAMED unfixed limit), the guarantee predicate, the nearest-manifest owner resolution, the prebuild-allowlist reader over every tracked file under `.github/` (a prebuild inside a PowerShell CI script guarantees a bin exactly as well as one in a `run:` block), the repo root taken from git IN THE INVOKED TREE rather than from the compile-time `CARGO_MANIFEST_DIR` (a prebuilt xtask would otherwise measure the tree it was BUILT in, and a verdict about the wrong tree is worse than none), the burn-down baseline with its write/compare/prune arms, and the two control arms; unit -- the guarantee predicate on all three of its wrong directions, the baseline delta reddening only on an unrecorded site while reporting a burned-down one (both asserted together, since a delta of nothing passes either alone), the path-kind reader, nearest-manifest ownership including the not-a-whole-component near miss, all four syntaxes with the line numbers the register already cites as evidence, comment blanking that does not shift the lines below it, the near misses that would each be a phantom site, prebuild detection refusing `--bins` (reading it as `--bin s` would fabricate a guarantee for a bin named `s`), and the false-positive arm's blind case with its refusal condition. PORT FIDELITY IS PART OF THE EVIDENCE: this replaces a python prototype whose numbers the register cites, and it was verified against that prototype site-by-site -- 185 tuples identical, not four summary integers, which can agree by coincidence -- with both run on ONE tree after proving the two candidate trees carried an identical `.rs` population. Kin REQ-XTASK-SPT-BIN-TARGET-DIR (the other xtask tooling defect about looking in the wrong place).
2026-09-06T13:09:02.2852973Z - Required stages: impl, unit
2026-09-06T13:09:02.2853216Z 
2026-09-06T13:09:02.2853323Z ### REQ-FIXTURE-BIN-PERJOB-DRIFT
2026-09-06T13:09:02.2866581Z - Title: The per-job companion to REQ-FIXTURE-BIN-BUILD-EDGE, and it is a DRIFT GUARD, NOT A FINDER. `xtask perjob-map` walks every CI job step by step and asks, for each narrow cargo invocation, whether the cross-package fixture bins its tests consume were built EARLIER IN THAT JOB -- by an explicit `cargo build -p X --bin B` or by an earlier workspace-shaped build. On this tree the answer is 7 narrow invocations, 0 unguaranteed, which is what establishes that the 11 repo-wide reds are NOT CI defects. THAT ZERO IS A FACT ABOUT TODAY'S WORKFLOWS AND NOTHING MORE, and the requirement says so because the tool's value is entirely in the FUTURE case: the day somebody adds a narrow invocation to a job that does not build what it consumes. A requirement that framed this as proof the class is closed would license deleting it the moment the register entry is written. THE ONE MODELLING TRAP, already paid for once: a workspace-shaped run emits the plain binaries only if it actually builds integration-test targets -- `--workspace` filtered to `-E 'kind(lib) + kind(bin)'` compiles lib/bin harnesses and emits NO plain fixture exe on a clean pool, which is exactly what `ci.yml:102-106` documents and hand-prebuilds around. The first prototype credited it as a builder and returned a GREEN over the one known-real gap; the kind-filter test is therefore a required unit row, not an optional one. THE SCRIPT-BLIND HOLE IS PRINTED IN THE COMMAND'S OWN OUTPUT RATHER THAN FOOTNOTED IN A DOC. The model reads `run:` blocks in workflow YAML; narrow cargo invocations ALSO live inside `.github/ci/*.ps1` and `*.sh`, which those blocks merely invoke, and no `run:` parser can see inside them. Rather than leave that as a limitation somebody has to remember, the scripts are scanned with the SAME detector and every narrow invocation they contain is listed as UNMODELLED with the cross-package bins it consumes: today 2 instances (`g6-curve.ps1`, `g6-postbounce.ps1`, both `-p spt-daemon --test inject_control_wedge`), both consuming NONE, so the hole is real and its instance count is measured at zero instead of asserted. An UNMODELLED invocation that DOES consume a cross-package bin counts as a gap and reds the command, because a verdict nobody can compute must not read green. THAT SCAN NEEDED ITS OWN CORRECTION, AND THE DIRECTION MATTERS: scanning the scripts raw read `reap-census.ps1`'s comment-based-help block -- which DESCRIBES a `cargo test -p spt --test ...` failure in prose -- as a real invocation consuming three cross-package bins, i.e. a fabricated gap inside the section whose entire job is honesty about what cannot be seen. PowerShell `<# #>` blocks and whole-line `#` comments are stripped; a TRAILING `#` deliberately is not, because in sh that would eat `${var#foo}` and losing a real invocation is the worse direction here. THE YAML IS PARSED BY A DELIBERATELY SMALL SUBSET READER, AND ITS FAILURE MODE IS ACCOUNTED RATHER THAN TRUSTED: xtask carries no parser dependency, and a silent mis-parse would DROP steps, after which every job those steps belonged to would read guaranteed -- a green manufactured by not looking. So the reader counts the `run:` command keys present in the file and refuses (exit 2) unless it attributed every one of them to a job. A `run:` that opens a mapping rather than a command (`defaults.run.shell`) is not a command key on either side of that count. LIMITS THAT REMAIN OPEN AND ARE NAMED IN THE COMMAND'S OWN DOC: `uses:` steps are not followed, and guarantee is tracked per job in step order without modelling artifact reuse across jobs on a persistent self-hosted workdir -- and our golden boxes ARE persistent, so a WORKSPACE verdict is sound only within its job and a clean runner could still expose what reuse masks. Gate: impl -- `crates/xtask/src/perjob.rs` and its `perjob-map` subcommand: the consumed-bin index over integration-test targets and over the unit population (unit tests get no `CARGO_BIN_EXE_*` at all, which is why `-E 'kind(lib)+kind(bin)'` is a CONSUMER step), the step walker with its prebuild and workspace-build guarantees, the accounted YAML subset reader, the script scan with its comment stripping, and the reuse of REQ-FIXTURE-BIN-BUILD-EDGE's detector rather than a second one (mapping through a different detector would produce a per-job verdict over a different population); unit -- the parser refusing an unattributable `run:`, block and inline bodies both surviving while `uses:` and `#` lines do not, cargo invocations stopping at a shell separator and `nextest run` not being read as the shorter `test` verb, the kind-filtered workspace run classified as consumer rather than builder, flag values requiring the whole flag (`--bins` is not `--bin s`), script prose not being read as an invocation, and a job guaranteeing by prebuild, by workspace build, and failing when neither ran. THE GUARD WAS OBSERVED FIRING ON A REAL FALSE STATE, not only in tests: while this lane was in progress the checker's own test fixture named `CARGO_BIN_EXE_git_fixture` unescaped in a tracked `src/` file, which entered the unit-consumer population and made this map report a gap in `ci.yml`'s unit lane and exit 1. Kin REQ-FIXTURE-BIN-BUILD-EDGE (the repo-wide half, whose detector and helpers this reuses).
2026-09-06T13:09:02.2880172Z - Required stages: impl, unit
2026-09-06T13:09:02.2880315Z 
2026-09-06T13:09:02.2880428Z ### REQ-ER-OFFLINE-BRINGUP-REACHABLE
2026-09-06T13:09:02.2889552Z - Title: A CLEANLY-OFFLINE ENGINE ROOM IS STILL BRINGABLE THROUGH ITS OWN CEREMONY: for the engine-room endpoint class with a bring-up code in hand, the rc offline fast-fail must not preempt the bring-up dispatch, because cleanly-offline is precisely the state whose ratified entry path IS a bring-up. Field-reported (releases#142, operator): after a harness exit the operator's third `spt rc engine-room` answered 'offline — nothing to attach to' for the same verb that had brought the room up on the first attempt, leaving the seat unrevivable through the only door it has. THE EXIT PATH IS CORRECT AND IS NOT WHAT CHANGES — a harness exit leaves the endpoint cleanly offline exactly as the ratified lifecycle entry requires (`mark_offline` → `terminal_normalize`); what is wrong is the reader's precedence, and the fix is a NARROWING of one gate rather than a change to any writer. REQ-HAZARD-RC-ATTACH-FAILFAST SURVIVES VERBATIM FOR THE GENERAL POPULATION and that survival is part of this requirement rather than a side note: the guard stops a real historical infinite-blank hang for every endpoint whose broker dance would end on a dead resolve, and the engine room is the one class whose dance does NOT end there — it would bring the seat up — so the guard's population was simply never narrowed to exclude the endpoint that has a revival path. Deleting the guard, or widening the exemption past (engine-room class AND a code presented), rebuilds that hang. THE ROW VALUE IS THE DISCRIMINATOR EVERY TEST MUST CARRY EXPLICITLY (hertz's one-field falsifier, ruled binding): a fixture whose `status` row happens to be ABSENT passes against this defect and proves nothing, so both arms — row PRESENT and offline, and the same fixture with the row cleared and nothing else changed — must be asserted, and they must reach the same outcome. RIDER, SAME LANE: every refusal arm reachable by the engine-room class names the ceremony surface (`spt rc <id>`, which prompts for a member-or-admin code) as its remediation, never `spt endpoint run` — that verb is not the engine room's entry surface and the reserved-id refusal already turns it away, so the printed line sends the operator (or a reasoning agent, which will invent a route from a bare no) at the wrong door. The zombie arm carries the same class-correctness bug and is swept with it. Gate: doc — the CONTEXT.md engine-room lifecycle sentence (a harness exit leaves the endpoint cleanly offline and re-bringable through the same TOTP gate), ratified 2026-08-04 and tagged rather than re-authored; impl — the narrowed gate and the two class-correct refusal remediations; unit — the gate table carrying the row value EXPLICITLY in both arms (engine room + code reaches the bring-up dispatch with the offline row present, and again with it cleared), the general population still fast-failing on an offline row (a non-engine-room endpoint, and the engine room WITHOUT a code — the code-less reconnect re-drive must not ratchet the attempt ledger), and each swept refusal naming the ceremony surface rather than `endpoint run`.
2026-09-06T13:09:02.2897609Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2897771Z 
2026-09-06T13:09:02.2897881Z ### REQ-ER-GRANT-ANNOUNCE-AFTER-ATTACH
2026-09-06T13:09:02.2904462Z - Title: A GRANT LINE DESCRIBING A CONTROLLER THAT NEVER ATTACHED MUST BE IMPOSSIBLE (ratified 2026-08-04 bag grill, releases#142): the engine room's bring-up announces empowerment — and spools the briefing that states it — only AFTER the controller's attach is established, never at the moment the session merely exists. Field-reported: the operator read 'the engine room is up; taking its controls. <grant line>' and then, seconds later, 'no live session for engine-room' — nothing lied about the session, the sentence claimed a CONTROLLER. The seam is precise and is what this requirement is about: the bring-up waits for the broker's own session table (the fact the attach needs), which is necessary and NOT SUFFICIENT — the controller is seated later, by a subscribe carrying the single-use admit ticket, arriving on the dispatcher's connection. releases#102's below-the-grant move was the right direction with the wrong stop: it moved the briefing below the GRANT, not below the SEAT. THE BRIEFING IS NAMED EXPLICITLY AND NOT ONLY THE PRINTED LINE, because they fail together and a fix that defers only the visible sentence leaves the seat holding a briefing that describes an empowerment for a controller that never arrived — the worse half, since the session reads it as fact about itself. The post-seat settlement already exists for the OTHER entry (an attach or take against an already-hosted engine room settles after the seat ladder resolves, grants only for a taken seat, and returns its sentence as the seat note the human reads before any session output); this requirement makes the bring-up complete through THAT path rather than through a second one. Gate: doc — the CONTEXT.md engine-room lifecycle sentence, ratified 2026-08-04; impl — the bring-up's announce and briefing composed on the post-seat side of the attach seam; unit — bring-up admitted but the admit ticket never redeemed (subscribe never arrives, or its TTL expires) leaves NO empowerment written, NO briefing spooled and NO grant sentence anywhere, while a confirmed seat produces all three exactly once, and a MEMBER-credential bring-up seats its controller while granting nothing (the discrimination that must survive wherever the settlement moves).
2026-09-06T13:09:02.2910291Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.2910448Z 
2026-09-06T13:09:02.2910542Z ### REQ-ER-SEQUESTERED-CWD
2026-09-06T13:09:02.2923923Z - Title: THE ENGINE ROOM LAUNCHES FROM A SEQUESTERED DIRECTORY OF ITS OWN, NEVER FROM WHEREVER A CALLER HAPPENED TO STAND (releases#178, operator-requested; doyle's four rulings 2026-08-19). The engine room governs the NODE and is not a participant in any user project, so inheriting a caller's cwd gave it an identity it should never have had: the recorded `info.cwd` is not an inert stamp but the anchor the context store routes its project tier from, the registry gossips project ids from, and the picker offers resume rows from — so an ordinary bring-up from a developer's checkout silently enrolled the node's governance surface as a member of that project. Its cwd is `$SPT_HOME/engineroom/cwd`, provisioned empty. IT IS DELIBERATELY NOT INSIDE THE OWLERY: a foreign directory sitting in the perch store risks reading as a ghost perch to any enumerator, which is a worse defect than the one being fixed. THE CWD IS FORCED, AND FORCED AT THE RESOLUTION SEAM RATHER THAN ONLY AT THE CALL: passing the directory at the spawn site is a declare-site fix, and a declare-site fix is not an enforce-site fix (the manifest `role.cwd` arm wins over a caller default by design), so for the reserved engine-room id the sequestered directory beats BOTH the caller's `spawn_cwd` and a manifest-declared `role.cwd`. A manifest that declares one is IGNORED LOUDLY — one log line naming the rule — and NEVER refused a bring-up: the operator must not be locked out of the room over a cwd, and an unstated deliberate override is indistinguishable from a bug to the next reader. THE ROUTING PREDICATE IS WIDENED FROM ITS SPELLING TO ITS MEANING: `is_owlery_internal` says owlery but asks 'is this a real user project?', and a cwd under `$SPT_HOME` but outside the owlery minting a synced `p-<dir>` project was junk-in-waiting independent of this change. Every consumer was walked before the widening (the condition on it): the context-store project routing, the psyche-notification routing, the registry's GOSSIPED project list, the shared `projderive` history kernel behind both the picker and the daemon's materialized index, and the picker's resume-row drop — ALL FIVE ask the same question and every one of them would be wrong to route, advertise or offer a sequestered engine-room cwd, so the predicate is widened in place rather than split in two, and the two duplicate copies of the normalize-and-prefix test are collapsed onto the one kernel in the same touch so they cannot drift apart afterwards. THE WIDENING IS MONOTONE — the owlery is a subdirectory of `$SPT_HOME`, so the new base is a strict superset and nothing previously excluded becomes included — which is what keeps ADR-0037 section 4's rendered-parity promise true for every real user project. DECLARED CHANGE, NOT A SIDE EFFECT: a project kept UNDER `$SPT_HOME` stops being routed into the synced backup/migrate set, stops being gossiped, and stops being offered as a resume row. READ-ONLY IS POLICY, HONESTLY LABELLED AS SUCH: the directory is provisioned empty and never written, and that is asserted by test — a same-user Windows ACL cannot enforce write-denial against a child running as the same user, and pretending otherwise would ship a guarantee the platform does not make; hard enforcement by integrity level or a separate token is its own future item and is deliberately NOT folded in. DECLARED RESIDUAL: the engine room is drop-less, so no relative drop-dir or commune-dir resolution work rides here; if a future feature gives the engine room file drops, THAT feature owns pinning them absolute, because a relative drop resolving against a never-written directory is the same failure class REQ-ECHO-DROP-DIR-RESOLVE already closed once. Gate: impl — the single-source path for the sequestered directory, its provisioning, the spawn site passing it, the reserved-id guard at the resolution seam with its loud ignore line, and the widened predicate repointed at every one of the five call sites; unit — the routing decision table written out in BOTH directions (an owlery-internal cwd routes no project, a `$SPT_HOME`-internal cwd OUTSIDE the owlery routes no project — the new row — and a genuine external directory still mints its `p-<dir>`), plus the resolution table proving the guard is engine-room-shaped rather than global (a declared `role.cwd` LOSES for the reserved id and still WINS for every other endpoint); int — a real bring-up launching in the sequestered directory, with NOTHING written into it across a full bring-up and briefing cycle, which is one assertion serving as both the drop-less witness and the read-only-policy witness.
2026-09-06T13:09:02.2938085Z - Required stages: impl, unit, int
2026-09-06T13:09:02.2938312Z 
2026-09-06T13:09:02.2938431Z ### REQ-SHELL-RELINK-FORCE
2026-09-06T13:09:02.2952645Z - Title: `spt shell relink --force` STOPS A RUNNING SHELL BINARY, PROVES IT STOPPED, AND ONLY THEN RELINKS -- and the already-online refusal stays the DEFAULT, because an unflagged relink that silently killed a running binary would make the recovery verb destructive. Today `relink` is the online switch for an OFFLINE instance and refuses `AlreadyOnline` when the DERIVED liveness probe (REQ-HAZARD-SHELL-STALE-ONLINE: local, record-independent) says the binary is really up; the field ask (releases#6) is the instance that IS up and needs a stop-then-restart without churning its canonical id through teardown+spawn. The stop half is the EXISTING link-break close (`shellhost::close_shell`, REQ-SHELL-2): pre_close instruction, close_timeout_ms termination window, the AUTHENTICATED force-kill (REQ-SHELL-KILL-AUTHENTICATED), link token retired to RETIRED_TOKEN_FILE so pending spool rows survive the rotation, drive slot + tunnel cleared. No second stop path is minted -- a force relink that closed the link any other way would be a second, divergent definition of a link-break. TWO REFUSALS ARE PART OF THE DELIVERABLE, NOT HARDENING AROUND IT. (1) EPHEMERAL REFUSES: `close_shell`'s divergence is `ephemeral => TornDown` -- perch and spool history ERASED, mint slot freed -- so a stop-then-restart built on it converts an ephemeral instance into a teardown and then relinks a record that no longer exists. Force therefore refuses on `ephemeral` with its own diagnostic BEFORE the close runs; an erase is irreversible and a refusal is recoverable, the same polarity KNOWN-HAZARDS applies to every perch-directory decision. THE REFUSAL NAMES THE ESCAPE (doyle, 2026-08-04): it states that an ephemeral instance has no stop-and-restart shape and points at the teardown + spawn pair that does what was meant, so the refusal TEACHES rather than dead-ends -- a consumer who is told only 'no' discovers the reason by losing a spool somewhere else. (2) A SURVIVOR REFUSES THE RELAUNCH: `close_shell` returns `Ok(Offline)` even when it printed `SHELL_CLOSE_SURVIVOR` -- the kill can be REFUSED by the pid+birth authentication (a recycled pid), or the binary can outlive the window -- and launching there yields TWO binaries for one instance, one of them unreachable through the retired token and invisible to the record. Force re-probes the recorded pid after the close and refuses the launch while it is still alive, naming the pid, rather than doubling the instance. THE RE-PROBE IS THE KILL PATH'S OWN AUTHENTICATION, NEVER A BARE PID-ALIVE CHECK (doyle, 2026-08-04): pid reuse is MEASURED on this fleet (a same-boot recycle inside 30s, both processes user-owned), so a bare probe manufactures a false survivor and the refusal then names an innocent process -- it runs `relay_liveness` over the pid+birth PAIR, the identical predicate `kill_shell_at` authenticates with, and the diagnostic states WHICH verdict it got (pair matched = our binary outlived the close; a live pid with no stamp = survivor and recycled pid indistinguishable; unprovable identity) so a reader can tell a true survivor from a stale record. The pair is captured BEFORE the close, because the close retires the record it lives in -- `close_shell` removes the pid file and an authentication-refused kill retires it too, so a probe reading the perch afterwards finds nothing and calls every survivor a success. A pid whose stamp DISAGREES reads Gone and RELAUNCHES: our binary is genuinely dead and the number was recycled, which is the case a bare probe would have called a survivor. Neither refusal is inferable from `CloseOutcome`, which is why the force arm carries them rather than the close. CROSS-NODE WITHOUT A WIRE BUMP: `shell_link_remote` already carries `args: &[String]` and the serve's SHELL_LINK_RELINK arm ignores args today, so an OLD peer handed a force flag runs a PLAIN relink and replies `ok: already online` -- the caller would read its force as honored. The discriminator needs no new op and no version gate: when force was requested, `already online` is a FAILURE, not the goal state. The serve's idempotent-ok reading of AlreadyOnline is correct for an unflagged relink and false under the flag, so the flag changes how the reply is read on both sides. Gate: doc -- the shells overview states the flag, both refusals, and the cross-node skew reading, since the ephemeral refusal is the one a consumer will otherwise discover by losing a spool; impl -- the flag on the CLI verb, the force parameter threaded through relink_shell_from/relink_shell, the pre-close ephemeral refusal, the post-close survivor probe, the remote arm passing the flag and re-reading `already online` as failure; unit -- an ephemeral instance is REFUSED and its perch SURVIVES the call (asserted together: a refusal that still erased would pass a return-value assertion alone), a surviving pid refuses the launch and NO second binary is spawned, an unflagged relink on a genuinely-online instance still refuses AlreadyOnline (the default is not vacuously widened), and a forced relink on an already-OFFLINE instance is the plain relink (the flag is not a second code path for the ordinary case); int -- the e2e stops a real running mock shell binary and relinks it under the SAME canonical id, with the pre-close/kill/relaunch ordering observable rather than assumed.
2026-09-06T13:09:02.2966525Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.2966702Z 
2026-09-06T13:09:02.2966816Z ### REQ-SEC-REPLY-EXEMPTION-SEND-LEG
2026-09-06T13:09:02.2976843Z - Title: THE REPLY EXEMPTION HAS A SENDING LEG, AND IT IS WIRED AT THE TRANSMISSION CHOKEPOINT RATHER THAN AT ANY CALLER. REQ-SEC-1's stateful firewall is a JOIN of two halves: the receiving leg classifies an inbound against the recent-outbound log (`classify_inbound`), and something must WRITE that log. It shipped with only the reading half live -- `note_outbound` existed, was `pub`, was re-exported, and had a passing unit test, while the whole `crates/` tree held ZERO production call sites (releases#166, confirmed at main@8248bc3: two callers, both tests). `RecentOutbound` was therefore never written in the field, every inbound classified `Unsolicited`, and a genuine reply to a closed-posture endpoint bounced -- the requirement was STRUCTURALLY INERT while reading fully covered. THE WRITE SITE IS `wan::request_wan`, THE ONE SEAM EVERY WAN MESSAGE CROSSES, NOT `wan_send_with` OR ANY OTHER CALLER: a per-caller write is a rule satisfied by remembering to, and the same forgetting is what made the first implementation inert; the chokepoint also puts both halves of the join in one file, where a future reader cannot change one without seeing the other. THREE THINGS THE ROW MUST GET RIGHT, EACH THE PROVABLE FIELD RATHER THAN THE CONVENIENT ONE. (1) THE ENDPOINT IS THE DAEMON-STAMPED `sender_proven`, NEVER `msg.from`: `from` is reply-routing metadata an explicit `--from` owns (KNOWN-HAZARDS 7.5), so a row keyed on it would let any local caller open a 15-minute inbound hole for an endpoint it merely NAMED -- the same reasoning that already keeps `from` out of the access chain's tier 1 and out of the contact ledger. An UNSTAMPED send records NOTHING: `session_proven_sender` stamps only after resolving the id to a real local perch, so the stamp is exactly the proof that the sending id can RECEIVE, and an id that cannot receive cannot be replied to. (2) THE NODE IS THE CONNECTION'S HANDSHAKE-PROVEN `remote_id_hex`, NEVER THE NODE RESOLUTION CLAIMED: the receiving leg keys the gate on the proven origin (REQ-HAZARD-WAN-ORIGIN-AUTH), and a correlation whose two halves keyed on different notions of `node` would silently fail to meet under exactly the stale-gossip conditions the registry is stale-tolerant by contract about. (3) ONLY A CUSTODY-CONFIRMED ROUND-TRIP RECORDS -- delivered, spooled, or duplicate, the same set that earns `SENT(WAN)`. A refusal must not write (their gate closed; a row would open OURS to a node that just refused us), a no-perch must not write (nobody is there to answer), and an UNCONFIRMED hop must not write even though an N-1 receiver may genuinely reply: an exemption is a WIDENED ADMIT VERDICT, so an unconfirmed hop failing closed costs one reply its exemption while the opposite direction grants a hole on a guess. Gate: impl -- the wire-in at `request_wan`, taking the dialed `NetDialed` rather than a bare conn id so the proven node is in hand, plus the split-out decision the units drive; unit -- the decision table: each custody outcome records and each non-custody outcome records nothing (all six named, so a widened set cannot pass), an unstamped send records nothing beside a stamped one that does (with `from` populated in the unstamped arm, so a fallback to it fails the assertion rather than passing it), and THE ROW IS THE KEY THE RECEIVING LEG READS -- the real write followed by the real `classify_inbound`, correlating on the recorded (endpoint, node) pair while a DIFFERENT local endpoint and a DIFFERENT origin node each stay `Unsolicited`; that meeting is the property whose absence made the requirement inert, and asserting the write alone would not have caught it. The proven-node half is COMPILE-ENFORCED rather than asserted: the seam takes the dialed `NetDialed`, so the resolved-node string the caller also holds is not in reach at the write site. int -- THE JOIN, not the parts: a REAL cross-node send from A to B, then B's reply reaching A's CLOSED-posture endpoint and passing with `PassReason::ReplyExemption`, with the same reply REFUSED in the un-sent control arm.
2026-09-06T13:09:02.2987169Z - Required stages: impl, unit, int
2026-09-06T13:09:02.2987311Z 
2026-09-06T13:09:02.2987423Z ### REQ-ACL-MUTATION-HUSK-REFUSAL
2026-09-06T13:09:02.2995687Z - Title: ADR-0053'S FAIL-CLOSED HAS A WRITE SIDE, AND IT IS ENFORCED AT THE SAVE SEAM RATHER THAN AT THE MUTATION CALL SITES. `load_checked` split NotFound from corrupt and the gate refused a husk correctly -- but the store is READ-MODIFY-WRITE at every path that edits it, and `AccessStore::load` is `load_checked_from(..).unwrap_or_default()`, so a degrade was swallowed into an EMPTY document. The consequence inverted the guarantee (releases#57): the gate refused unsolicited traffic, and then the first `spt endpoint access` command an operator ran to investigate the refusal saved that empty document over the damaged file -- permanent rule loss, and a posture that flipped from degraded-closed to open-with-one-rule. The boundary held exactly until someone touched the CLI, and the command most likely to be run is the one an operator reaches for BECAUSE the gate is refusing. THE SWALLOWED DEGRADE IS THEREFORE CARRIED ON THE DOCUMENT (`StoreOrigin::Degraded`, `#[serde(skip)]`, never part of the record) AND `save_to` REFUSES IT. This placement is the requirement, not an implementation detail: converting the nine production mutation call sites to `load_checked` closes the population that exists today and leaves the next read-modify-write path free to re-open the class -- the same shape of defect as REQ-SEC-REPLY-EXEMPTION-SEND-LEG's per-caller write, which shipped inert because a rule satisfied by remembering to is satisfied until someone forgets. Every write of this document funnels through `save_to`, so a refusal there binds call sites that do not exist yet, and an audit is not what holds it. The call sites ARE converted as well, for the diagnostic: a refusal at the load names the store before anything is attempted, which reads better than one raised by a save the operator already believes succeeded. FRESH IS NOT DEGRADED: a document constructed in memory -- the minted baseline, a test fixture -- stands in for nothing on disk and stays writable, so mint-on-first-absence (the split that keeps an unconfigured fleet reachable) is untouched. The refusal names the store path per ADR-0053, names the cause, and names the way out, which is real rather than rhetorical: a store the operator MOVES ASIDE is re-minted empty by the next command, so starting from empty stays available as a deliberate, loud choice -- which is precisely what silently rewriting it from empty was not. Gate: impl -- `StoreOrigin` on `AccessStore` set by the loaders (`load_from` marks the swallowed degrade, `load_checked_from` marks a parsed store `Loaded`), the `save_to` refusal in the single voice of `husk_write_refusal`, and the nine production mutation sites loading checked (cli.rs `endpoint access` allow/revoke/open + subnet-mint capture + endpoint purge, api/engineroom.rs `enforce`/node-mode/node-surface-mode, daemon pairhost.rs join-time capture); unit -- a decision table over the three origins whose expectations are WRITTEN OUT rather than derived from the code under test, asserting on-disk bytes and not merely the returned Result: a husk-derived document refuses the write and the damaged file survives BYTE-FOR-BYTE, a loaded document's read-modify-write preserves the rules it did not touch, a fresh document writes, and the refusal names the store path.
2026-09-06T13:09:02.3004317Z - Required stages: impl, unit
2026-09-06T13:09:02.3004470Z 
2026-09-06T13:09:02.3004588Z ### REQ-ACL-POSITIONAL-ALLOW-HONORS-FLAGS
2026-09-06T13:09:02.3014998Z - Title: `access allow`'s POSITIONAL SPELLING REACHES THE SAME MUTATION SEAM ITS FLAG SPELLING DOES -- it is a shorthand for the rule, never a second, ungated way to write one. The v1 arm matched `node: Some(..)` and swallowed every other flag in a `..`, then called `AccessStore::allow` directly, and that cost three things at once, all three RUNTIME-MEASURED before the fix (releases#185; the filing was read-derived and named only the first). (1) `--surfaces MSG` WAS DROPPED, and an empty surface list means EVERY surface -- so the store took a rule strictly WIDER than the operator typed, exit 0, no diagnostic on either stream. The direction matters and was checked rather than assumed: the `is_empty => set_all(Mode::Closed)` at the tail of `allow_surfaces` reads `acl.modes`, the POSTURE table, and has nothing to do with the surfaces argument, so this falls as an over-grant and not as a silent close. (2) `--origin user` WAS DROPPED THE SAME WAY: a rule the operator restricted to one caller class matched both. (3) CALLING `allow` DIRECTLY WALKED AROUND `apply_mutation`, AND WITH IT BOTH THE `--admit-node` ACKNOWLEDGMENT AND THE GRANT-NODES POLICY -- the gravest of the three and the one no reading of the issue predicted. Measured as a three-arm differential under ONE authority (an owner agent, which requires a real perch record and not merely the env var): the flag spelling REFUSED the unacknowledged grant, the same flag spelling with `--admit-node` was accepted, and the positional spelling then wrote the WIDER rule for that same agent at exit 0. A gate a respelling steps around is not a gate, and the respelling was two words shorter than the gated one. THE FIX IS THE SEAM, NOT THE FLAGS: the arm builds its rule through the shared `build_access_rule` -- so the two spellings cannot drift into two parsers -- and applies it through `apply_mutation` with the invocation's real authority, which is what makes the acknowledgment and the policy bind here by construction rather than by a second copy of them. WHAT STAYS v1 IS THE POSTURE: this spelling RESTRICTS the endpoint (closes its blanket mode when it sets none yet), the seam is deliberately posture-blind, so the close is re-applied explicitly through one shared body (`restrict_if_unset`) that `allow_surfaces` also calls. `iff unset` is load-bearing -- an endpoint that already names a posture has been configured, and admitting one more node must never overwrite that. Whether the FLAG spelling should close a posture too is releases#196 and is deliberately NOT decided here; preserving this arm's close is a non-regression, not a new opinion. THE CONSEQUENCE IS ACCEPTED AND DECLARED (doyle, 2026-08-18): an owner agent running the bare positional spelling is now REFUSED without `--admit-node`, which is the gate working as designed and what the flag spelling already demanded; a same-node user is untouched, since the acknowledgment binds the owner-agent authority alone. Gate: impl -- the arm destructuring its flags instead of discarding them, the shared builder, the `apply_mutation` route carrying `admit_node` and the real authority, and `restrict_if_unset` as the single body of the posture-close; unit -- a decision table whose expectations are WRITTEN OUT AS LITERALS rather than derived from the code under test: the typed surface list lands on the persisted rule and an untyped one still means every surface, an origin qualifier survives, the unacknowledged owner-agent grant is REFUSED THROUGH THIS ARM while the same-node user's is not (the bypass, pinned as a refusal rather than as an audit), the refusal leaves the store BYTE-UNCHANGED, and the posture-close still happens for the positional arm while an endpoint that already names a posture keeps it; int -- THE JOIN, which is where this defect actually lived: the real binary, a real store on disk, the operator's typed list read back off the persisted RULE, with an untyped arm beside it (the empty list is a legitimate spelling and must survive the fix) and the unacknowledged owner-agent refusal paired with the same grant acknowledged, so the flag is the only difference between the two.
2026-09-06T13:09:02.3025586Z - Required stages: impl, unit, int
2026-09-06T13:09:02.3025736Z 
2026-09-06T13:09:02.3025851Z ### REQ-ACL-PRECISE-ALLOW-WRITE-CONTRACT
2026-09-06T13:09:02.3038727Z - Title: THE PRECISE `access allow` IS A RULE VERB; THE POSITIONAL ONE IS A RULE+POSTURE VERB -- and the precise arm therefore says so at write time rather than leaving the operator to discover it. releases#196 asked whether the flag spelling should also close an endpoint's posture on its first rule (the v1 semantic `restrict_if_unset` carries, which the positional arm re-applies explicitly). RULED HORN B (doyle 2026-08-21, on a census read at b88fab2a): default-open-until-explicit-close IS the shipped T6 semantic, the mutation seam stays POSTURE-BLIND, and what was missing was never the close -- it was any signal that a rule written onto an unrestricted endpoint changes no verdict. TWO STRUCTURAL FACTS DECIDE THIS RATHER THAN A PREFERENCE. (1) A CLOSE CANNOT LIVE AT THE SEAM: `tuple_mutation` is shared by the endpoint precise arms AND the engine room's node-wide seat including its per-endpoint `--for` form, and below it `apply_mutation` is also the seam for knock-answer, redeem and the knock store -- so a close in `apply_mutation` would fire on every approved knock and every redeem, and a close in `tuple_mutation` would have the engine room closing another endpoint's posture as a side effect of writing one rule for it, while being meaningless for `MutationScope::Node` (`restrict_if_unset` is per-endpoint by construction). A posture close is a CLI-ARM opinion either way; it is not, and cannot be, a property of the seam. (2) THE CODEBASE ALREADY RULES THE INERTNESS IN BINDING GATE CODE: `may_grant_node_subject` derives the unset grant-nodes policy from `effective_posture` with the premise stated in its own comment -- open implies permitted, because an allow entry on an open surface punches no hole. Closing the posture inside the very command that policy gates would falsify that premise one statement later, so horn A would have owed a re-ruling of the grant-nodes policy and was never the one-line change it reads as. WHAT THE NOTICE IS. On an operator-driven precise allow, and BEFORE the write, the chain is ASKED -- `decide()` per covered surface, per admitted origin class, at every concrete origin there is to ask about -- and if it already allowed that subject everywhere, the acceptance is followed by a sentence saying the rule stands and changes no verdict today, naming the lever that would make it bite. NEVER A REFUSAL: pre-positioning a rule before a later close is legitimate and common, and a verb that refused it would be wrong more often than the silence it replaced. ASKED, NEVER RE-DERIVED: the condition is the chain's own verdict and not a scan of rule rows, the doctrine `node_still_sees_endpoint` and `fork_without_discover` already carry -- a row scan would miss a per-surface close, count a row naming a different subject, and become a second definition of a verdict the store owns. AN ABSENCE IS NEVER A VERDICT: a subnet subject with no roster members, and a sender-endpoint subject that resolves to no machine this node knows, have NO origin to put the question to, so they get a NAMED ABSENCE saying the question was not answered -- never the word inert, which would render a missing measurement as a finding. A subnet subject with members is inert only if EVERY member reads inert. THE FIRING POPULATION IS DECLARED AND PINNED, not left to placement: operator-driven precise ALLOW upserts on an endpoint scope, and nothing else -- the engine room's `--for` seat is excluded by the seat flag the caller already passes, removals and denies are excluded by op and decision, and knock-answer and redeem never reach this body at all. NO NOTICE ON DENY THIS LANE (doyle, ruled): deny is the fail-closed direction, an inert deny pre-positions defense at zero cost, and the narrowed-deny shape is un-censused. POSTURE-NEUTRALITY IS PART OF THE CONTRACT AND IS PINNED AS SUCH: the precise arm leaves `modes` exactly as it found them, which was true at b88fab2a and untested in either direction -- a ruling no test can falsify is a ruling that survives its own reversal. Gate: impl -- the population predicate, the chain-asked probe over resolved origins and its named-absence arm, the notice line naming the lever, and the call site before the write inside the precise arm; unit -- a decision table whose expectations are WRITTEN OUT AS LITERALS rather than derived from the predicate under test: an allow onto an unrestricted endpoint reads inert, the SAME allow shadowing a node-scope deny reads NOT inert (the hole-punch negative control -- the case the notice must stay silent on), a rule whose surfaces are only partly refused reads not inert, an empty subnet reads NAMED ABSENCE and never inert, a subnet whose members split reads not inert, and the population predicate admits the operator's precise allow while refusing the daemon seat, the removal and the deny; int -- the real binary against a real store: a precise allow leaves the endpoint's posture EMPTY (the literal partition, not a value read back from the code under test) and prints the notice, the positional spelling beside it still closes the posture and prints NO notice, and the NEGATIVE POPULATION PIN -- a rule written through the knock-approval path prints no notice at all.
2026-09-06T13:09:02.3052177Z - Required stages: impl, unit, int
2026-09-06T13:09:02.3052338Z 
2026-09-06T13:09:02.3052458Z ### REQ-HAZARD-EMPOWER-SESSION-RESURRECT
2026-09-06T13:09:02.3063442Z - Title: AUTHORITY MUST NOT SURVIVE AT REST ACROSS A SESSION-ID ROTATION. An engine-room empowerment is session-state (ADR-0052: empowerment is session-state, not stored grants; nothing to revoke at rest), keyed on the session id the engine room's perch is bound to and filed under that session's scratch directory. 'spt api boundary' rotates exactly that bound session id, and it is NEITHER of the two revocation edges ADR-0052 names (controller detach, local take), so before this requirement a rotation left the empowerment record filed under the OLD session id, unreachable by the seat and unswept by anything. MEASURED, not inferred (todlando red probe, 2026-08-03): rotate to a session, empower, rotate away, DROP the authority at the new session, then rotate BACK onto the old id — the seat holds the subnet again with no admin code proved since the drop. THE SCOPE IS AUTHORITY OUTLIVING ITS SESSION, NOT A STRANGER GAINING AUTHORITY: 'api boundary' is auth-gated to the perch's own harness, so the actor re-acquiring is the actor that held the grant, and this must not be re-told as privilege escalation. The empower module's own comment asserted the opposite — that a stale file is 'unreachable by construction' and that 'if this sweep never ran, no caller would gain a single grant' — which is a claim about the resolver's INPUTS and is false while an arbitrary '--to-session-id' is accepted past a guard that only refuses psyche-custody squats. THE FIX IS A MOVE, NEVER A COPY (doyle ruling): a boundary CARRIES the empowerment to the new session id, and when the boundary returns the old session's record is GONE — two records for one grant is the same leak wearing a different hat. IT FAILS CLOSED: if the move cannot be completed the boundary DROPS the empowerment with a loud diagnostic and never leaves the old record standing as a fallback. THIS REQUIREMENT CLOSES THE BOUNDARY EDGE ONLY AND MUST NOT BE READ AS CLOSING THE RESURRECTION CLASS: two further mechanisms rotate a bound session id and leave the prior session's record standing -- the dead-owner re-pin (api/auth.rs) and an establish_perch re-bind -- and their polarity is the OPPOSITE, because a boundary is ONE session lineage continuing while those are a NEW session taking over a perch whose prior owner is gone, so the authority does not follow, it DROPS. They are filed as releases#138 and are not evidence for or against this requirement. Gate additions ruled with the fix: the carry is GATED on the endpoint that can hold an empowerment, because the record is keyed by SESSION ID ALONE and an ungated carry would let an unrelated endpoint's boundary move the seat's live grant out from under it -- so one row proves the gate is not too tight and a SECOND proves it is not too loose; and the fail-closed arm is proved by INJECTING a removal failure and asserting the old record still grants nothing, since an arm no mutation can fail is untested. THE HYGIENE SWEEP IS NOT THE MECHANISM AND MUST NOT BE WIDENED INTO ONE — it is documented hygiene-only, it runs at grant time, and a re-acquire happening long afterwards is past it either way; the boundary's own move is the only thing that closes this. Gate: impl — the carry-as-move at the boundary rotation with its fail-closed drop and diagnostic; unit — the probe above, whose DISCRIMINATING construction is dropping the authority at the new session BEFORE rotating back (without that, a plain rotate-away/rotate-back cannot separate resurrection from the ruled carry, under which the grant legitimately follows the rotation), plus the assertion that the old session's record is absent the moment the rotation returns, so the two versions are distinguishable at the mechanism and not merely at the outcome.
2026-09-06T13:09:02.3073141Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3073307Z 
2026-09-06T13:09:02.3073401Z ### REQ-CLI-STACK-HEADROOM
2026-09-06T13:09:02.3079883Z - Title: THE CLI MUST HAVE ROOM TO BUILD ITS OWN ARGUMENT TREE, WITH MEASURED HEADROOM. clap's derive-built `Command` tree is constructed recursively at the top of every invocation and its cost scales with the argument population; `main` ran `cli::run()` on the MAIN thread, whose stack on Windows is the PE-header default (1 MiB, not settable at runtime), while a Rust-spawned thread's is explicit. IR-33 measured the tree within roughly SIX net new arguments of exhausting it: U1's eight hidden bool args made EVERY invocation die with 'thread main has overflowed its stack', exit -1073741571 (0xC00000FD), INCLUDING `--version` — before any of our own code ran. The failure face is why this is a requirement and not a tuning note: it presents as a totally broken binary that names no stack, and through REQ-DOCS-5's generator (which enumerates the tree by PARSING the binary's own root help) it presents instead as a silently gutted docs page that the drift check then compares to itself and blesses. FIX: run the CLI on an explicitly-sized thread, so the argument population stops being a budget the next verb-surface lane has to fit under. THE WRAPPER MUST BE FAITHFUL, WHICH IS THE WHOLE RISK OF THE CHANGE: a panic inside the CLI must still leave the process with the SAME face it had when the panic unwound out of main — the same message through the same hook, and the same nonzero exit code (101) — and an ordinary return must exit with the INNER run's code, not the join's. A spawn failure falls back to running inline and SAYS SO, because a resource blip must not turn into a dead CLI, and a silent fallback would restore the old ceiling invisibly. Gate: impl — the sized-thread wrapper in main.rs with its fidelity mapping and its loud fallback; unit — the wrapper driven with a returning closure and a PANICKING one, asserting the code passes through and the panic maps to 101 (the prod call site passes `cli::run` to the same function, so this is the real wrapper and not a parallel spelling); int — a smoke that the DEBUG-built binary answers `--help` with non-empty stdout at exit 0, which is the only assertion that would have caught IR-33 in its own build profile and also backstops the empty-emit generator above.
2026-09-06T13:09:02.3085693Z - Required stages: impl, unit, int
2026-09-06T13:09:02.3085850Z 
2026-09-06T13:09:02.3085956Z ### REQ-USHER-LIFECYCLE-VERBS
2026-09-06T13:09:02.3097900Z - Title: THE ENDPOINT LIFECYCLE READS AS VERBS, AND `endpoint run` RETIRES WITHOUT A SHIM. One overloaded verb carried the whole lifecycle: `spt endpoint run` minted an endpoint, started a new session on an existing one, resumed a prior session, opened the interactive picker, and (with `--save`) set a startup default — the invocation's MEANING sat in which of nine flags were present, so the CLI could not be read and the picker's doors could not be named. The ratified surface (releases#5 bag grill, 2026-08-04) is: `endpoint create <new-id> [--subnet S] [--adapter A] [--cwd DIR]` = the ONLY mint, subnet immutable after it; `endpoint start <id> [--adapter A] [--cwd DIR]` = a NEW session on the endpoint's most-recent adapter in its most-recent project folder, NEVER the env cwd, and an UNKNOWN id REFUSES pointing at `create` so a typo cannot mint a phantom endpoint; `endpoint resume <id>` = its LATEST session; `endpoint auto-start <id> [--off]` = the standalone lever that was `run --save`; and top-level `go <id>` = the operator's take-me-to-this-endpoint ladder (online+uncontrolled -> rc; online+controlled -> interactive kick confirm over `rc --take`; suspended -> wake then rc; offline WITH sessions -> resume then rc; offline WITHOUT sessions -> mint the first session then rc; engine-room -> defers to the gated `rc engine-room` path). `go`'s offline discriminant reads the SESSION LEDGER (a non-provisional row) and NOT the record's `session_id`, which persists by design after a clean stop as the CAS identity anchor and is never a liveness claim. RETIREMENT IS A CLEAN BREAK: `endpoint run` is removed through a RAW-ARGV PRE-SCAN ahead of `Cli::command()` so its nine args never enter the derive tree, and it answers with a parse error naming the replacement verb — no hidden variant, no silent alias. TWO CAPABILITY NARROWINGS ARE DELIBERATE AND MUST NOT BE RE-ADDED QUIETLY: specific-session resume (`run --resume <session>`) retires with the verb and has no replacement, `resume` being latest-only; and the picker's two argv prefill quadrants (`--adapter`-only pre-select, `--id`-only create-new prefill) retire with their verb, the id-only case's replacement being `start <unknown-id>`'s refusal. `create` MINTS ONLY — skeleton perch, permanent home, recorded adapter/cwd defaults — and does NOT start a session; the first session is `start` (or `go`, which also attaches), so a fresh bringup is TWO commands where `run --start` was one. That is a shape change to every bringup script rather than a lost capability, and it rides the release notes beside the resume narrowing. It was decided rather than read off the table (todlando, ratified by doyle 2026-08-04) on the ladder's own evidence: a first-class offline-WITHOUT-sessions rung is near-unreachable unless zero-session endpoints are routine, so create cannot be starting one. The picker's own door is BARE `spt` on an interactive terminal and this requirement does not touch it, nor does it add a `pick` verb. Generated launcher shortcuts bake the retired argv, so the generated body becomes `spt go <id>` (the launcher's intent IS `go`'s ladder, and it opens a console so the kick confirm has its TTY), the shortcut SENTINEL is bumped so a stale launcher is detectable rather than merely broken, and the refusal text names BOTH the new verb and shortcut regeneration. REGISTRY SUPERSESSION: this requirement is the authority on the bringup SPELLING, and it supersedes every earlier title in this file that names `endpoint run`. Those were swept where they state a CURRENT contract or a CURRENT gate's invocation; where they narrate a PAST failure or a past field observation they are left verbatim, because a failure record rewritten into a spelling that did not exist when it happened stops being a record. Arg budget: the ratified surface is the MIN spelling at 11 leaf args against the retired 9, a net +2 that is only affordable because the pre-scan keeps the retired args out of the tree — and it rides on REQ-CLI-STACK-HEADROOM having deleted the ceiling rather than budgeted under it.
2026-09-06T13:09:02.3108112Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3108278Z 
2026-09-06T13:09:02.3108431Z ### REQ-CLI-NODE-VERB-PRIMARY
2026-09-06T13:09:02.3118648Z - Title: THE PER-MACHINE SUPERVISOR SURFACE IS SPELLED `spt node`, AND `spt daemon` REMAINS A FULL ALIAS OF IT — ONE PARSE TREE, NOT TWO. The noun operators type for their own machine was `daemon`, which names the PROCESS rather than the thing the process supervises; the surface is a node's state (daemon state, member subnets, local endpoints), so `node` is the primary spelling and `daemon` is a clap `visible_alias` on the same command. THE ALIAS IS FULL AND PER-SUBCOMMAND, NOT ROOT-ONLY: bare (the status view), run (incl. the hidden --detached auto-start-ladder flag), start, stop (incl. --force), status, access + its allow/deny/remove editing arms, refresh, and the hidden brain entry (bare and stamped) all parse identically under either spelling. A root-only equivalence proves the alias resolves and proves NOTHING about a subcommand added later under one spelling, which is why the unit asserts the parsed action as a VALUE per subcommand rather than matching variants. THE CHANGE IS CLI-SURFACE-ONLY: the spt-daemon crate, the wire, and the docs model term `spt-daemon` are untouched, and `daemon access` is mirrored as a NOUN, not duplicated as policy — one parse tree, one roster store. THE DEPRECATION NOTE IS SHARED AND ALIAS-NEUTRAL (doyle fork-D ruling): clap renders help per COMMAND, not per alias, so the note lives in the one `///` block and is worded to be TRUE under both spellings — deprecation when read on `daemon --help`, information when read on `node --help`. No argv sniffing, no stderr-only notice, no second variant. THE BRAIN SPAWN ARGV KEEPS THE `daemon` SPELLING (doyle fork-A ruling, KH-2.3): brain_child_args builds `daemon brain --generation N --start-reason R`, and the compatibility it exists for runs toward binaries that PREDATE the alias — an N-1 parser knows only the old spelling and an alias cannot rescue a parser that never had it, so migrating the spawn site is a later milestone gated on a fleet version floor, never this lane. REMOVAL OF THE ALIAS IS BLOCKED ON AN INSTALL-ARTIFACT MIGRATION (doyle fork-C ruling) AND THAT CONSTRAINT RIDES THE DOC SURFACE, not this file alone: installed OS service units and scheduled-task rungs on every deployed machine already carry `spt daemon run`, and renaming the verb does not rewrite them, so a future milestone must migrate those artifacts before scheduling removal. THE DEPRECATED SPELLING IS DOCUMENTED, NOT HIDDEN (doyle fork-E ruling): `visible_alias` rather than the hidden `--deferred`-style precedent, so the alias reaches the generated reference as a findable deprecated row — a deprecation an operator cannot find is not a deprecation. Gate: impl — the primary/alias rename with the shared note, the dispatch arm, the surface-help path table moved to the primary spelling, and the KH-2.3 spawn-argv sentence; unit — per-subcommand alias equivalence over the enumerated surface, the bare form defaulting to the status view under both spellings, and the note rendering in BOTH spellings' long help (the shape-(i) choice made visible, so moving the note to a runtime print reds here); doc — CONTEXT.md's disambiguation line separating this VERB from the retired `spt-node` separate-process deliverable, plus the reference regen carrying the deprecated row and the removal constraint. Kin REQ-CLI-2 (the noun this re-spells), REQ-HAZARD-HANDOFF-ARGV-COMPAT (the spawn argv it must not modernize), REQ-DOCS-5 (the generator whose drift gate must be green on the regen).
2026-09-06T13:09:02.3127428Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3127581Z 
2026-09-06T13:09:02.3127794Z ### REQ-WAN-DENY-PRECEDES-EXISTENCE
2026-09-06T13:09:02.3133726Z - Title: At the WAN ingress, an ACCESS DENIAL and an ABSENT PERCH are DISTINCT OUTCOMES, and the denial arm runs FIRST. `receive_wan` consults the gate before it asks whether the target's perch exists, so a refused arrival returns `WanOutcome::Refused` whether or not the target is there, and `WanOutcome::NoPerch` is reserved for an ADMITTED arrival with nothing to deliver to. The ordering is the load-bearing half and it is not an accident of layout: reversed, a denied message to an absent perch would report absence, which both mislabels the refusal AND is the precise shape that would make a later `DENIED`-style reword of the NO_PERCH line leak the existence of an endpoint deliberately hidden from the asker (the DISCOVER-denial seam, where `Existence is not advertised` is ratified three times over in `wansend.rs`). The distinctness half is what the two outcomes MEAN downstream: `Refused` rides the wire as `refused` and renders `WAN_REFUSED` at the sender, `NoPerch` rides as `no_perch` and renders the absence line, and an operator diagnosing a silent peer acts differently on each. TWO LANDED UNITS ALREADY LEAN ON THIS UNNAMED � `the_stamp_not_the_from_is_the_sender_subject` and `an_admitted_arrival_records_contact_and_a_refused_one_does_not` both seed NO perch and both assert `Refused` (and its absence) at the ingress, so their green depends entirely on the deny arm preceding the existence test while nothing states that it must. This requirement names the dependency and pins it. MEASURED 2026-08-19 (releases#171 measurement, todlando): with the perch ABSENT in both arms and the deny rule as the only variable, denied-msg => `Refused` and allowed-msg => `NoPerch`. Gate: impl � the deny arm at the head of `receive_wan`, ahead of the perch-existence test; unit � the one-variable both-arms measurement, which fails if either the ordering or the distinctness is broken. Kin REQ-MSG-SENDER-STAMP (whose tier-1 subject decides the deny), REQ-ACL-DISCOVER-DEFAULT-ON (the seam whose camouflage this ordering protects).
2026-09-06T13:09:02.3138866Z - Required stages: impl, unit
2026-09-06T13:09:02.3139098Z 
2026-09-06T13:09:02.3139193Z ### REQ-VOCAB-ANCHOR-SUBNET
2026-09-06T13:09:02.3150467Z - Title: THE UBIQUITOUS LANGUAGE FOR AN ENDPOINT'S DEFAULT-SCOPE SUBNET IS `anchor subnet`, NOT `home subnet`. Operator-requested (discord:reavo) and GREENLIT as releases#176: `home` reads as a place the endpoint lives, which is precisely the reading the concept must not carry — identity is node-global and advertised into EVERY subnet the node belongs to, and the subnet in question anchors DEFAULTS (sync scope, bare-name qualification) rather than bounding where the endpoint exists. The glossary body already used the word `anchor` to explain what `home` meant, which is the tell: the definition needed a second word to undo the first. THIS INVERTS A RATIFIED GLOSSARY AVOID AND MUST SAY SO INLINE. CONTEXT.md carried an `_Avoid_` line forbidding the term `anchor subnet`, ratified at the 2026-07-28 access-control grill; that line becomes current-truth `anchor subnet` CARRYING A DATED SUPERSEDE NOTE naming both dates and the greenlight, because a bare inversion is what a future grill re-derives as drift (doyle, 2026-08-19). It is an ALIGNMENT rather than a reversal in spirit: operator vocabulary had already drifted this way — SPT_MANTLE was called the engine-room ANCHOR on 2026-08-05 — and the note is what stops the next reader re-litigating it. THE RENAME STOPS AT THE SERIALIZATION BOUNDARY, and that boundary is the whole risk of this requirement. `PerchInfo.home_subnet` is an ON-DISK serde key in every perch `info.json` and rides the pairing/wire path; renaming it would make every existing perch read as anchor-less and every N-1 daemon lose the key. The field KEEPS ITS NAME so the key keeps its spelling by identity — no `#[serde(rename)]` to maintain and no chance of a later rename silently changing the wire (an `alias` would have been WORSE THAN NOTHING: it is READ-compat only and would still WRITE the new spelling — doyle's correction, 2026-08-19). The freeze is stated AT the field, so the next reader meets the reason where the exception lives. DATED RECORDS ARE NOT REWRITTEN: ADR filenames and titles stay (ADR-0010 `immutable-home-subnet`, ADR-0026 `multi-subnet-home`), and their BODIES keep their words too, each gaining a short dated vocabulary note instead — a decision record states what was decided when, and re-spelling its prose would leave the project unable to tell an amended record from an original one. REQ ids carrying HOME (REQ-RUN-PICKER-HOME) are identifiers on the same footing as ADR filenames and do not move. RECORDS ARE NOT SWEPT, and the ROADMAP's delivered-milestone log and the dated `docs/F-0xx` design notes are records on the same footing as the ADRs — they state what was delivered or designed when, under the word in use at the time. WHAT DOES MOVE: every LIVE surface a person reads — CONTEXT.md, PRD, docs/MANIFEST.md, the docs-site operator prose, CLI help and doc comments, picker titles and legends, and ALL THREE user-visible diagnostic tokens, which become `ANCHOR_NOT_MEMBER`, `NO_ANCHOR_SUBNET` and `ANCHOR_REFUSED` — a diagnostic is the loudest vocabulary the CLI has and would otherwise teach the retired word at exactly the moment an operator is looking the concept up, and renaming two of three would read as a typo rather than as a decision. `ANCHOR_REFUSED` is asserted by two e2e files, whose assertions move in the same change. Gate: doc — the CONTEXT.md glossary term renamed with its inline dated supersede, the docs-site/PRD/MANIFEST prose, and the dated vocabulary notes on ADR-0010 and ADR-0026; impl — the CLI/help/picker text, the three diagnostic tokens, and the stated freeze at the serde field; unit — the renamed tokens and picker text are asserted at their own seats AND the retired spellings are asserted ABSENT, because a partial sweep that leaves one surface behind reads as a typo rather than as the boundary this requirement draws.
2026-09-06T13:09:02.3160593Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3160740Z 
2026-09-06T13:09:02.3160860Z ### REQ-ER-BRINGUP-LAUNCH-PHASE-TRACE
2026-09-06T13:09:02.3168021Z - Title: A BRING-UP THAT FAILS NAMES THE PHASE IT REACHED, in a record that outlives the operator's terminal. The bring-up handler waits on ONE fact — a row in the broker's own sessions table — and the launch that produces that row runs on a DETACHED THREAD which, pre-fix, said nothing at all unless it returned Err. So the only account of a failure was the refusal sentence `the code was accepted, but the engine room's harness did not come up within 30s`, which reaches the operator's terminal and dies with it, and which is IDENTICALLY produced by four different mechanisms: a launch that returned Err (loud, but only in a channel nobody read against the timeout), a launch still waiting UNBOUNDED on the broker's reply (`Brain::cold_start` carries `io_timeout: None`), a ConPTY create that overran the bound, and a harness that SPAWNED AND DIED so that its row existed and then did not. THE POLL CANNOT TELL THESE APART BY CONSTRUCTION — it reads a row, and every one of them is a missing row. The requirement is that each side stamps its own phases into the persisted daemon stderr sink (REQ-DAEMON-STDERR-PERSIST): the launch thread stamps enter/prepared/connecting/connected/requested/spawned and its three failure arms, the shared spawn choke stamps received/gate_claimed/pty_creating/pty_created and each step between the PTY existing and the row being inserted, the exit waiter stamps the row's REMOVAL, and the poll stamps the instant it starts waiting and the instant it gives up. Every span is read MONOTONICALLY off one `Instant` per side (`elapsed`), never as a difference between two clocks, so a near-boot reading cannot underflow. AN ABSENCE IS NEVER A VERDICT: a missing outcome stamp whose PRECEDING stamps stand is a wait still in flight, while a missing outcome whose predecessors are ALSO missing is an unreadable instrument and must be reported as that — which is why the integration stage asserts every rung renders end to end through the diagnostics panel on a GREEN bring-up, before any silence on a cold run is read as a fact about the launch. Gate: impl — the phase stamps on the brokered launch path, in the shared spawn choke, at the exit waiter's row removal, and either side of the bring-up ready-wait; int — a successful bring-up whose panel carries every rung of both ladders, so the instrument's silence is falsifiable rather than assumed.
2026-09-06T13:09:02.3174394Z - Required stages: impl, int
2026-09-06T13:09:02.3174546Z 
2026-09-06T13:09:02.3174657Z ### REQ-IPC-DEADLINE-UNHONORABLE-REFUSED
2026-09-06T13:09:02.3184110Z - Title: A BRAIN-IPC READ HANDED A DEADLINE ITS CARRIER CANNOT HONOR REFUSES BY NAME AT THE READ PRIMITIVE, never silently unbounded. `Brain::read_frame_until` takes `deadline: Option<Instant>` and dispatches on the carrier: the `Split` (pump) arm honors it as a total-wait `recv_timeout` on the remaining budget, and the blocking `Whole` arm CANNOT honor it at all — a timeout there would have to abandon a half-read frame, which is a desynced carrier, and that is precisely why REQ-HAZARD-PUMP-IPC-DEADLINE built the reader-thread carrier rather than reaching for a socket timeout (interprocess on Windows named pipes has no portable read timeout and its `set_nonblocking` corrupts mid-stream — measured at that requirement). So the fix is NOT to make the Whole arm bounded; the fix is to stop the Whole arm from ACCEPTING a bound it will not keep. Pre-fix it dropped the parameter on the floor and returned a blocking read, so a caller that asked for a bounded read got an unbounded one WITH NO DIAGNOSTIC — the caller's own deadline was the thing that vanished. THE INVARIANT IS TWO-SIDED AND BOTH SIDES ARE LOAD-BEARING: an ABSENT deadline STAYS ABSENT (the ordinary blocking read on a `Whole` carrier is untouched, and no default bound is fabricated for it — the absent-is-not-zero shape ruled on releases#201), while a PRESENT deadline on a carrier that cannot honor it becomes a NAMED, DISTINCT error naming the carrier arm and the remedy. THIS MOVES AN EXISTING RULE TO THE SEAM EVERY VERB CROSSES rather than inventing one: `digestlink::refuse_unbounded_carrier` already refuses a `Whole` brain for the digest-pull verb, and its own doc comment records why a comment was not enough — 'it already fooled one implementation of this very function'. One verb was guarded; every other deadline-taking verb (`read_event_until`, `broker_image_version_until`, `read_controller_event_resuming_until`, `read_peer_reply_until`) reached the same primitive unguarded, which is the declare-vs-enforce shape: the rule was declared in three doc comments and enforced at one call site. AT c62904e7 NO PRODUCTION CALLER PASSES A DEADLINE ON A WHOLE CARRIER — the rc pump, the peer pump and the digest pull all build `cold_start_pump`, and the two deadline-aware wrappers document that production passes `None` — so this is a latent unbounded read one construction away, filed as such rather than as a field incident. Gate: impl — the `Whole` arm of `read_frame_until` returns the named refusal for `Some(deadline)` and the unchanged blocking read for `None`, plus the one-line amendment at `refuse_unbounded_carrier` recording that the primitive now enforces what its comment used to only warn; int — three carrier arms measured together, because any one of them alone is satisfiable by a wrong fix: `Whole`+deadline must RETURN (the witnessed red, which pre-fix blocks to its kill bound), `Split`+deadline must STILL time out at its deadline (the positive control a blanket refusal would break), and `Whole`+`None` must still round-trip a real request (the negative control against a fabricated default). Kin REQ-HAZARD-PUMP-IPC-DEADLINE (the carrier split this protects) and REQ-DIGEST-CROSS-NODE-PULL (the one verb that guarded itself).
2026-09-06T13:09:02.3192336Z - Required stages: impl, int
2026-09-06T13:09:02.3192477Z 
2026-09-06T13:09:02.3192584Z ### REQ-ER-INBOUND-LOCK-ALL-PATHS
2026-09-06T13:09:02.3210145Z - Title: THE ENGINE ROOM'S INBOUND LOCK REACHES EVERY SPT-AUTHORED DELIVERY PATH, NOT ONLY THE WIRE. The lock itself (REQ-ER-INBOUND-LOCK) was never wrong: it sits at step 0 of the access chain, ahead of the same-node allow, and its own comment names an agent on this very node as the adversary it exists for. The defect was that no locally-authored delivery ever ASKED it. A census of every path terminating in a local delivery (releases#209, measured at c62904e7) found 17 paths and exactly 2 consulting the chain — `wan::receive_wan` and `dispatch::answer_presence_probe`, both wire-inbound — while `crates/spt/src` and `crates/spt-msg/src` between them contained ZERO occurrences of `access_check`, `InboundClass` or `classify_inbound`. The bypass was therefore WIDER than the filing's 'a same-node agent send': `spt ring`, the subnet notify, the API-side drain and the relay listener are each an independent arm to the same three termini (the shared PTY-inject leg, `deliver_tcp`, and the spool). THE FIX IS ONE GATE SITE, and it is a site rather than a restated rule because a rule with N call sites is a rule that will disagree with itself at the seam nobody re-read — this codebase already says so at `engineroom.rs`'s reserved-id predicate, for the same reason. `spt_store::gate::admit_local_delivery` is that site, and it LIVES IN `spt-store` FOR A STRUCTURAL REASON THAT IS PART OF THE REQUIREMENT: `spt-msg` authors local delivery and cannot see `spt-daemon`, so a function beside `wan::deliver_admitted` would have been unreachable from half the paths it exists to gate. THE GATE IS AT ADMISSION, NEVER AT DRAIN, and the engine room's own session briefing is what decides it: that briefing is daemon-authored and self-addressed, spooled straight onto the ER perch under a reserved author, so a drain-time gate would refuse the engine room its own briefing. Admission-time exempts it for free because that writer never crosses the function — AND THAT EXEMPTION IS THE CALL PATH, NEVER A ROW FIELD, so a send merely CLAIMING briefing authorship is gated on its merits like any other. Two further facts settle admission-vs-drain: a spooled row carries no admission stamp and a locally-spooled one has nothing to re-derive an origin from, and a WAN row is already claimed exactly-once by `op_id`, so re-checking at drain would re-open a settled decision and evaporate a row admitted while the posture was Online if the controller detached before the tick. THE GATE ASKS WHETHER THE TARGET RESOLVES TO THE ENGINE-ROOM PERCH, NOT WHETHER IT IS SPELLED LIKE IT. `is_engine_room` is an exact compare, correct for every caller asking about the NAME; it is the wrong question for a gate, because delivery routes by resolving an id to a perch. MEASURED on NTFS 2026-08-22: create `engine-room`, and `Engine-Room` names the same directory — that spelling misses the broker's session table (which does compare exactly), falls to the spool keyed by resolved perch path, and the drain delivers it, so the bypass would have survived its own fix. The predicate compares canonicalized paths and mints NO canonicalizer of its own: imitating the filesystem's case rule would be a second reading of one truth and would be WRONG on Linux, where the other spelling is a genuinely different perch with no engine room at it. The unresolvable arms are declared rather than left to fall out: the exact spelling is checked first and needs no filesystem, so the reserved id is caught with no perch on disk at all; a target that does not canonicalize is not the ER, because nothing is there for a delivery to land on; and an ER perch that does not canonicalize means no engine room is provisioned, for which the posture read behind the predicate is independently fail-closed. THE REPLY EXEMPTION MUST SURVIVE ON THE LOCAL PATH, and pre-fix it could not have: `classify_inbound` correlates against `RecentOutbound`, whose only production writer was `note_outbound` at ONE call site on the WAN outbound leg, so a locally-authored send recorded nothing and every local reply to the engine room would have classified Unsolicited and been refused — the engine room able to ask a local agent a question and structurally unable to hear the answer. The admission site records the local outbound half, on an ADMITTED send only: a refusal is not contact and must not earn a reply window. A LOCAL REFUSAL NAMES ITS ORIGIN IN WORDS rather than printing this node's own key hex, which on the local path reads as a wire arrival and sends the reader hunting a peer that does not exist. SCOPE, STATED RATHER THAN IMPLIED, and the published claim is amended to match: admission runs in the AUTHORING process, so an old or modified binary, a direct write into the spool database, or a raw TCP connect to a relay listener never meets it. Same-user local code is outside what any spt gate holds; the claim is scoped to spt-authored delivery paths, and the TOCTOU between the gate's path resolution and the delivery's own falls under the same boundary. UPGRADE BOUNDARY: rows spooled before the flip carry no admission stamp and drain ungated exactly once. Gate: impl — the admission function, the resolve-based ER predicate, the local `note_outbound` half, the local-origin refusal text, and the scope amendment on the published claim; unit — the resolution fact (both spellings reach one perch, asserted as RESOLUTION rather than as platform behaviour so the cell stays portable), the briefing-authorship forgery negative (a local send wearing the reserved author is still refused), and the unresolvable arms; int — an unsolicited local send to the engine room refused with the lock's own diagnostic where pre-fix it delivered, the engine room's question to a local agent answered (reply exemption alive on the local path, loud arm silent on it), and the hole-punch negatives: ordinary endpoint-to-endpoint local sends still deliver, and still deliver with a subnet-scoped DENY rule and a closed node mode in the store. Kin REQ-ER-INBOUND-LOCK (the lock this reaches), REQ-SEC-1 (the reply exemption it must not break), REQ-ACL-LOCAL-ORIGIN-TIERS (the axis it runs on).
2026-09-06T13:09:02.3225631Z - Required stages: impl, unit, int
2026-09-06T13:09:02.3225790Z 
2026-09-06T13:09:02.3225890Z ### REQ-ACL-LOCAL-ORIGIN-TIERS
2026-09-06T13:09:02.3241757Z - Title: WHERE AN INVOCATION CAME FROM IS DATA THE CHAIN REASONS ABOUT, NOT A SHORT-CIRCUIT BEFORE IT. Until releases#209 the chain's step 1 was `if local, return Allow(SameNode)`, so locally-authored traffic never reached `AccessStore::decide` at all. That was invisible while local traffic was ungated, and it is exactly what would have foreclosed the operator's intra-node governance surface (releases#211) — which under the ruled shape is NOT a new tier between the per-endpoint entries and the node tier, but SELF-REFERENTIAL Node subjects at the EXISTING tiers (v1's slot-3.5 reading withdrawn in full, operator-rephrased 2026-08-22). A tier the local path short-circuits past is not a tier. So `AccessRequest` carries an `Origin` SET AT EXACTLY ONE CONSTRUCTION SITE — the chain's own — and never re-derived per call site, because a discriminant with two derivations is one that will eventually disagree with itself. THE LOCAL VARIANT CARRIES THE ORIGIN ENDPOINT ID, not merely the fact of locality: same-node governance discriminates by SENDER at the EXISTING tier-1 `SenderEndpoint` subject, so the id has to reach `decide` as part of the origin rather than be re-derived at each call site. Dropping it at construction would cost a second plumbing pass through this very seam — and releases#215 measures precisely what re-derivation costs when it is left to call sites: three feeders hand this one parameter three different value shapes, and a tier-1 rule silently cannot match on one of them. It is carried from the first commit even though nothing consumes it yet, and it is carried in the VARIANT rather than beside it so that constructing a local origin without deciding what to put there is not expressible. Its provenance differs from the wire's and the type says so: a remote sender endpoint is DAEMON-STAMPED and session-proven, while a local one is SESSION-DETECTED in the authoring process — never the caller-supplied `from`, which an explicit `--from` owns and which would admit a forged subject (KNOWN-HAZARDS 7.5, the same reasoning that keeps `from` out of tier 1). The same-node allow is DEMOTED to the chain BOTTOM, where it always semantically belonged: nothing named this traffic, so it is the same-node operation it always was. THE FIX MUST WIDEN NOTHING, and two tiers would have widened it silently if left alone. (1) THE MODE TIERS ABSTAIN FOR A LOCAL ORIGIN. A mode is a blanket posture about who may reach this node FROM OUTSIDE it; `closed` has never in this codebase's history been asked about traffic authored on this machine, because such traffic never reached the function. Letting the modes answer now would convert every `closed` posture in the field into 'and also stop the agents on this box talking to each other' — the hole-punch failure arriving through a door nobody opened deliberately. (2) A LOCAL ORIGIN CARRIES NO SUBNETS. This node is a member of its own subnets, so handing them in would let a subnet-wildcard DENY — written to keep some peer group out — begin refusing locally-authored traffic the moment that traffic reached the chain. Only rules that NAME the local origin explicitly speak about it; everything else falls to the same-node bottom, so today's local behaviour is preserved BY CONSTRUCTION rather than by inspection. A STORE DEGRADE MUST NOT CLOSE THE NODE IN ON ITSELF: ADR-0053's fail-closed refusal protects the node from the network, and applied to local traffic it would stop the agents here talking to each other every time `access.json` was mid-write, so a local origin takes the chain bottom on a degrade. The engine room is unaffected by that arm — its lock is step 0, above all of this, and its own posture read is independently fail-closed. A NODE WITH NO IDENTITY reads as local: nothing can be proven remote either, since a wire arrival carries a handshake-proven hex by construction (REQ-HAZARD-WAN-ORIGIN-AUTH), so an empty origin can only have come from a local authoring path — and that is also the fail-open direction for the same-node hole-punch. THESE SEMANTICS ARE NOT DECIDED HERE. This requirement owes #211 a reachable slot and a carried subject, nothing more: the ORIGIN AXIS and the local sender it carries. NO NEW TIER AND NO MODE TWIN ARE OWED — the v1 shape (a slot 3.5 with a mode twin 6.5, whose twin would refuse local traffic rather than abstain) is WITHDRAWN IN FULL, operator-rephrased 2026-08-22: #211's intra-node governance is SELF-REFERENTIAL Node subjects at the EXISTING tiers, rules-only, with modes-only-abstain staying exactly as this requirement built it. The ER lock stays at step 0 regardless, because same-node governance is a rules surface and never a replacement for the ratified engine-room semantics. Gate: impl — the `Origin` axis on `AccessRequest` set at one site and carrying the local sender id, the mode-tier abstention, the empty local subnets, the demoted same-node bottom, and the local degrade arm (the v1 slot markers are NOT part of this requirement's evidence and are removed with the withdrawal above — a marker describing an unbuilt tier certifies a design, not an implementation); unit — the hole-punch negatives with a CONCRETE adversary rule in the store (a subnet-scoped DENY present, and a closed node mode present, and a local send still admitted under each), plus a remote send under the same store still refused, which is what proves the abstention did not simply disable the tiers rather than scope them. Kin REQ-ACL-SUBJECT-CHAIN (the tier order this inserts into), REQ-ER-INBOUND-LOCK-ALL-PATHS (the lane that needed the axis), REQ-SEC-1.
2026-09-06T13:09:02.3255678Z - Required stages: impl, unit
2026-09-06T13:09:02.3255828Z 
2026-09-06T13:09:02.3255926Z ### REQ-ACL-INTRA-NODE-SELF
2026-09-06T13:09:02.3266895Z - Title: INTRA-NODE GOVERNANCE IS A SELF-REFERENTIAL NODE SUBJECT AT THE EXISTING TIERS, NOT A NEW TIER. Operator-rephrased 2026-08-22, superseding in full the v1 reading (a slot-3.5 rules tier with a mode twin at 6.5): #211 adds NO tier, NO Subject kind and NO schema change. A rule whose subject is THIS NODE'S OWN id governs traffic authored on this node, and it does so through machinery that already shipped. Two effects, both operator-confirmed: a NODE-SCOPE own-node rule governs intra-node actions for every endpoint hosted here, and a PER-ENDPOINT own-node rule governs incoming actions from other same-node endpoints. The third -- endpoint-targeted rules evaluate before node-targeted -- is the chain's existing order and nothing moves. THE MECHANISM AND THE CLAIM ARE DIFFERENT THINGS, AND ONLY THE MECHANISM WAS ALREADY PINNED. REQ-ACL-LOCAL-ORIGIN-TIERS stopped local traffic short-circuiting above `AccessStore::decide` and its unit cell `a_rule_naming_the_local_origin_still_governs_it` already proves a node-scope DENY naming the local origin refuses a local MSG, and that the RULE tier answered rather than a mode. THAT CELL CANNOT PROVE THIS REQUIREMENT, AND ITS OWN HELPER IS WHY: `req_local` constructs `Origin::LocalNode` DIRECTLY and carries an ARBITRARY origin string ("beef"), never `local_node_hex()`. So it proves the chain governs a request ALREADY classified local whose origin string a rule HAPPENS to name. This requirement's claim is a JOIN of two facts that sit on opposite sides of that helper: the local authoring path SUPPLIES THIS NODE'S OWN HEX as `origin_node` (`gate::admit_local_delivery` passes `local_node_hex()`), and therefore the string an operator actually WRITES -- their own node's key hex -- is the one that matches. MEASURED AT 2b855595: no test in the tree, unit or integration, constructs `Subject::Node { node: <own hex> }`; `er_inbound_local.rs` writes a `SubnetWildcard` DENY, which is the hole-punch negative and a different claim. The join has no unit form for the same reason REQ-ER-INBOUND-LOCK-ALL-PATHS's did: the authoring verb decides what reaches the chain, and only the real binary runs the authoring verb. RULES-ONLY, AND THE MODES KEEP ABSTAINING FOR LOCAL ORIGIN exactly as REQ-ACL-LOCAL-ORIGIN-TIERS built them -- the hole-punch property (a closed node/endpoint/subnet mode never severs the box's own agents) holds by construction, and #211's original default-deny example is expressed as a node-scope own-node DENY on MSG rather than as a posture. PRECEDENCE ABOVE THE CHAIN IS UNCHANGED AND RESTATED RATHER THAN IMPLIED: the engine room's inbound lock is step 0 and a self-referential rule never touches it (ADR-0052); a correlated local reply is admitted with no rule of its own (REQ-SEC-1), own-node deny included; and KNOCK is its own surface, so an own-node MSG deny leaves knocking to KNOCK's own evaluation. AN OWN-NODE SUBJECT CAN NEVER MATCH A REMOTE ORIGIN -- hex inequality, and the classification puts any request whose origin equals the own hex on the LocalNode arm before the chain runs. LOCAL GOVERNANCE IS IMPOSSIBLE BY CONSTRUCTION ON A NODE WITH NO IDENTITY, and that is a design property rather than a hole: `admit_local_delivery` passes `local_node_hex().unwrap_or_default()`, so an unprovisioned node hands in an EMPTY origin_node -- it still classifies LocalNode and still flows, but no rule can name a hex that does not exist. An unprovisioned node is exactly the state a first-run operator is in, so the property is asserted rather than left to be reported as a bug later. Gate: int -- the JOIN, driven through the real binary with no daemon: an ungoverned local send ADMITTED as the control on the same store, then refused under a NODE-SCOPE rule whose subject is the hex the binary itself minted, then refused again under a PER-ENDPOINT rule with the node-scope rule CLEARED so it cannot be the thing answering; each rule read back OUT OF THE STORE by match count rather than echoed from the argument, and each refusal asserted to have spooled NO row, so a slow delivery cannot pass as a refusal. Kin REQ-ACL-LOCAL-ORIGIN-TIERS (the axis this runs on, and whose synthetic-origin helper is why this evidence is owed), REQ-ACL-SUBJECT-CHAIN (the tier order it uses unchanged), REQ-ER-INBOUND-LOCK-ALL-PATHS (the admission site the local path crosses), REQ-SEC-1 (the reply exemption that outranks an own-node deny).
2026-09-06T13:09:02.3278123Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.3278300Z 
2026-09-06T13:09:02.3278395Z ### REQ-SEAL-RECORD
2026-09-06T13:09:02.3284018Z - Title: A WAX SEAL IS A DURABLE RECORD BINDING CONTENT TO A MINTER: `{token, content_hash, minter, minted_at, ceremony_kind}` — the token (REQ-SEAL-TOKEN-FORMAT), the lowercase-hex SHA-256 over the EXACT BYTES presented at the ceremony (the artifact-digest convention, `sha256_hex` house style: no prefix, no truncation), the FULLY-QUALIFIED minter address `subnet:endpoint@node` (the subnet component IS the binding subnet — the record carries no separate subnet field, and the store derives scoping by parsing the minter through the spt-proto address grammar, one truth not two), the mint timestamp in epoch ms, and the ceremony kind as an open string (`totp` arrives W2; FIDO2 is fast-follow; test-seam mints name themselves honestly and never wear a ceremony they did not run). NO EXPIRY AND NO REVOCATION IN V1 (operator ruling 6, 2026-08-23): the timestamp is in the record and a reversed decision is a NEWER seal, so records are IMMUTABLE ONCE MINTED — which is what makes the replicated store a join-semilattice for free (REQ-SEAL-STORE-REPLICATES-SUBNET-SCOPED). A seal is EVIDENCE, never an authorization subject by itself (the KNOWN-HAZARDS 7.5 discipline transported): nothing grants on token possession; only a BOUND verify verdict (REQ-SEAL-VERIFY-CONTENT-BOUND) is citable. W1 lands the mint machinery behind an INTERNAL SEAM ONLY — no public mint verb, no ceremony; W2 wires the TOTP ceremony to that seam. The seam refuses EMPTY content: a seal over zero bytes proves nothing and the verify surface treats empty stdin as absent content, so admitting the mint would create a record whose verification is indistinguishable from the no-content refusal. Gate: doc — the ratified CONTEXT.md wax-seal entry carries the record shape; impl — the record struct + mint seam in spt-store; unit — roundtrip persistence, minter-parse-derived binding subnet, empty-content mint refusal.
2026-09-06T13:09:02.3289256Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3289427Z 
2026-09-06T13:09:02.3289523Z ### REQ-SEAL-TOKEN-FORMAT
2026-09-06T13:09:02.3294263Z - Title: A SEAL TOKEN IS 8-10 CHARS FROM A NARROW UNAMBIGUOUS LOWERCASE CHARSET, minted by rejection-sampling the OsRng-backed identity source (the knock-code precedent — one randomness source, one audit point; no `rand` crate edge). The charset is the 30-glyph set `23456789abcdefghjkmnpqrstvwxyz`: digits drop `0`/`1` (collide with `o`/`l`/`i`), letters drop `i`/`l`/`o` (same collisions) and `u` (the Crockford accidental-obscenity drop) — REQ-HAZARD-ID-CHARSET's discipline applied to a NEW, NARROWER alphabet rather than reusing the endpoint-id set, because a seal token is read aloud and retyped by humans citing a decision, and `:`/`@` stay permanently reserved address delimiters so a token can never smuggle address structure. MINTS EMIT 10 CHARS (~49 bits over the 30-set — ample for a citation handle whose forgery is harmless by construction, since verify recomputes the content hash and possession grants nothing); VALIDATION ADMITS 8-10 so the format has headroom without a flag day (the sptkc_ versioning lesson: widen by admitting, never by breaking). Well-formedness lives in the STORE beside the record (the knock `is_well_formed` precedent) and every verb validates the token at its seam before touching the store — a malformed token is a refusal, never a lookup miss, so the two cannot be conflated by a caller. Gate: doc — the CONTEXT.md entry's 8-10-char clause; impl — charset const + validator + mint sampler in spt-store; unit — charset membership (every minted char in-set, ambiguous glyphs refused), length bounds both edges, malformed-vs-unknown distinction at the verb seam.
2026-09-06T13:09:02.3298638Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3298800Z 
2026-09-06T13:09:02.3298913Z ### REQ-SEAL-STORE-REPLICATES-SUBNET-SCOPED
2026-09-06T13:09:02.3307490Z - Title: SEAL RECORDS LIVE IN SUBNET SECURITY MATERIAL AND REPLICATE SUBNET-SCOPED, so `spt api seal verify|describe` answers on ANY member node of the binding subnet. The store is `identity/trust/seals.json` beside the grant store and whitelist (one JSON document, atomic write, the grants.rs template; a new path fn in perch.rs per REQ-HAZARD-SINGLE-PATH-SOURCE). PREMISE CORRECTION, MEASURED AT INTAKE (0fcbc086): the grant store and whitelist are NODE-LOCAL AND DO NOT REPLICATE — their 'propagation seam' is aspirational prose with no wire family behind it — so 'same plumbing' is honored by riding the ONE shipped subnet-scoped replication pattern, the notif store's four-part shape: (1) MONOTONE JOIN — the apply point is insert-if-absent by token, so records commute, a replayed feed no-ops, and immutability (REQ-SEAL-RECORD) makes the store a join-semilattice with no delta bookkeeping; a same-token-different-content arrival is DROPPED WITH THE EXISTING RECORD KEPT and counted as an anomaly, never merged, because tokens are minted once and a collision is either astronomical or adversarial, and the adversarial copy is already harmless (verify recomputes the hash). (2) KIND-TAGGED WIRE RECORD — NDJSON with kind = 'seal' (the knockmsg N-1 lesson: an untagged record lands in an old daemon's WanMessage arm as a chat message; a tagged one drops cleanly). (3) STREAMFAMILY REGISTRATION — a `Seal` family arm in classify_first_line + serve dispatch, widening the census array the bijection test asserts. (4) APPLY GATE + FULL-RE-PRESENTATION EMIT + PUMP WORKER — the gate admits a row only when THIS node is a member of the record's binding subnet AND the handshake-proven origin node is on that subnet's roster (fail-closed: non-member subnet drops, untrusted origin drops — the REQ-NOTIF-1/REQ-MESH-5 posture verbatim); the emitter re-presents every record of the subnet (duplicates are free under the join); the pump worker fans per member subnet x roster peer through the existing fan_targets discipline. DEGRADE POLARITY IS OPEN-TO-EMPTY (the grants polarity, chosen deliberately): a husk or corrupt store loads empty, so verify answers unknown-token — the store failing can only LOSE the ability to prove, never mint false evidence, which is the safe direction for an evidence primitive. Binding-subnet scoping is derived from the minter address at the ONE parse helper on the record — the emitter and the gate share it, so the scope a record replicates under and the scope it is admitted under cannot diverge. Gate: doc — the CONTEXT.md entry's subnet-scoped replication clause; impl — store + wire record + family arm + gate/emit/worker; unit — join idempotence + collision-drop, gate's verdict arms (admitted, non-member-subnet dropped, untrusted-origin dropped, replay no-op), emit/decode roundtrip, husk-loads-empty. int at gate: a record minted on one member node answering describe on another.
2026-09-06T13:09:02.3315183Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.3315349Z 
2026-09-06T13:09:02.3315558Z ### REQ-SEAL-VERIFY-CONTENT-BOUND
2026-09-06T13:09:02.3325762Z - Title: `spt api seal verify <token>` IS CONTENT-BOUND: the content arrives on stdin, the verb recomputes the lowercase-hex SHA-256 over the exact bytes received and compares it to the record's `content_hash`, and the verdict is BOUND or NOT-BOUND with the record's fields printed alongside — a verify that does not read content is not a verify, so BARE VERIFY WITH NO CONTENT (empty or absent stdin) REFUSES BY NAME, pointing the caller at `describe` (operator ruling 7: `SEAL_VERIFY_NO_CONTENT`, the refusal names the verb that answers the question actually asked). EXIT CONTRACT, PINNED SO A SCRIPTED CALLER CAN NEVER MISREAD A NON-VERDICT AS EVIDENCE: exit 0 IF AND ONLY IF BOUND. NOT-BOUND prints its verdict line (with the expected-vs-presented hashes) on stdout and exits nonzero; an unknown token refuses `SEAL_UNKNOWN_TOKEN` on stderr; a malformed token refuses at the format seam (REQ-SEAL-TOKEN-FORMAT) — three distinct texts, none of them exit 0, because ONLY A BOUND VERDICT IS EVIDENCE (KH 7.5: the token, and any envelope attribute carrying it, is never an authorization subject by itself; forged or wrong tokens are harmless by construction precisely because this verb recomputes the hash). The verbs are READ-ONLY and take no auth gate (the EndpointInfo precedent): the store they read replicates subnet-scoped, so membership already bounds who can answer, and a verify mutates nothing. Gate: doc — the CONTEXT.md entry's verify clause; impl — the api verb, hash recomputation at the store's one helper, the no-content and unknown-token refusals; unit — BOUND on exact bytes, NOT-BOUND on a one-byte delta, empty-stdin refusal pointing at describe, unknown-token refusal distinct from malformed-token refusal, exit-0-iff-BOUND across all five arms. #220 (operator, FIELD-SEAL W2): TWO CHANGES, NEITHER OF WHICH WEAKENS THE BINDING. (1) THE MINT'S TRIM IS MIRRORED AS A SECOND CANDIDATE: `seal mint` seals `raw.trim()` (both ends, the CRLF rationale of REQ-HAZARD-ENVELOPE-CR-LINESAFE), so presented bytes carrying a shell's trailing newline — every `echo |` on the fleet — could never bind the seal minted from that same text. Verify hashes the EXACT bytes FIRST and, ONLY ON A MISS, hashes the mint-trimmed candidate: additive by construction, so nothing that binds today stops binding, including content deliberately sealed WITH its whitespace. The trim MIRRORS THE MINT rather than the ticket's narrower 'trailing' — `str::trim` for UTF-8 content (the identical function the mint calls), ASCII whitespace for bytes the mint verb could never have produced but the store can hold. (2) THE VERDICT'S RECORD BLOCK IS THE NARROW ONE: `token`, `content_hash` and `ceremony_kind` DO NOT surface from verify — the verdict line already carries the token (`SEAL_BOUND:<token>`) and, on a mismatch, both hashes in prose, and a field repeated beside its own verdict reads as a second, weaker answer; `describe` keeps all three. Verify's minter rides VERBATIM (the human render is describe's alone). Gate adds: unit — exact bytes bind, trailing-newline/CRLF/both-ends-padded bind through the fallback, a real one-byte delta and interior-whitespace differences still NOT-BOUND, a seal minted over trailing whitespace still binds its exact bytes, the non-UTF-8 arm, and the three dropped fields pinned as an ABSENCE.
2026-09-06T13:09:02.3334758Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3334929Z 
2026-09-06T13:09:02.3335030Z ### REQ-SEAL-CEREMONY-TOTP
2026-09-06T13:09:02.3344325Z - Title: EVERY PRODUCTION SEAL MINT IS GATED BY A TOTP HUMAN-PRESENCE CEREMONY, AND THE WHOLE DECISION RUNS DAEMON-SIDE (write-through-daemon, doyle W1 design-review finding + approval 2026-08-23): the attached controller collects the code and ships the PRESENTED CODE — never a verdict — up the attach channel; the daemon verifies a member-or-admin code of the BINDING subnet locally against SubnetStore seed_bytes/admin_seed_bytes through code_matches_window's plus-minus-one-step window, BOTH seeds tried unconditionally and combined bitwise so the gate's timing cannot name which key admitted (the bring-up pattern; which key admitted is deliberately not distinguished — a seal grants nothing key-specific). The attempt is bounded by ITS OWN persisted ledger `trust/seal-ceremony-gate.json` — the GateLedger shape reused verbatim (one counter, one-second-doubling-to-one-hour backoff, throttle evaluated BEFORE verification so a shut gate is not an oracle, reset on success, survives daemon restarts, loud notification past the third failure) — SEPARATE from the bring-up and empower ledgers per the denial-of-governance rationale: failed seal guesses must not shut the gate a human needs to take the controls back, and vice versa. Only PRESENTED guesses count: Esc-cancel and absent input spend nothing (REQ-SEAL-CEREMONY-ESC-CANCEL owns the cancel arm). Ceremony success is the ONLY production caller of spt_store::seal::mint_seal (ceremony_kind `totp`), and the mint's load-modify-save runs IN THE DAEMON PROCESS under the same SEAL_APPLY serialization as the replication apply arm — a CLI-process minter would race the apply arm last-writer-wins, and a lost LOCAL mint is the one record re-presentation cannot heal (only the minting node ever held it). The record's fully-qualified minter is `subnet:endpoint@node` where the NODE HALF is the daemon's own NODE-KEY SHORT HEX (registry::key_prefix's roster short form — the durable node spelling ADR-0054 payloads carry, already resolvable by the @node grammar) — NEVER the OS hostname (doyle W2 gate ruling 2026-08-23: a hostname is mutable — the advertised label re-derives at daemon startup — and non-unique, so a durable evidence record citing it goes ambiguous between hostname-sharing nodes and orphans on rename; the pubkey remains the identity). A node always has its key by mint time (daemon boot mints node.key), so there is no ordinary no-identity refusal; an unreadable key is an IO anomaly refused fail-closed. Gate: doc — the CONTEXT.md wax-seal entry's ceremony clause; impl — the ceremony IPC verb, the daemon verify + ledger + mint-under-SEAL_APPLY arm, the seal ledger path helper; unit — member code admits, admin code admits, wrong code refuses and counts, throttle-before-verify ordering, in-backoff attempt uncounted, cancel uncounted, ledger file distinct from the bring-up and empower files, mint persisted by the daemon writer with the minter's node half pinned to the node-key short form (8 hex chars, never a hostname), loud threshold.
2026-09-06T13:09:02.3352334Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3352492Z 
2026-09-06T13:09:02.3352598Z ### REQ-SEAL-CEREMONY-CONTENT-SHOWN
2026-09-06T13:09:02.3358573Z - Title: THE CEREMONY OVERLAY SHOWS THE HUMAN EXACTLY WHAT THEY ARE SEALING: the sealed content VERBATIM plus the BINDING SUBNET NAMED — always the subnet whose member-or-admin code the overlay expects, so the human reaches for the right key (operator ruling 5's overlay clause) — and once W3 wires sealed dispatch, the destination id as well. A blind mint is a consent hole (doyle design-of-record 2026-08-23; releases#7's own sketch shows the content). The content bytes the overlay renders are THE bytes the daemon hashes at mint — same byte buffer end to end (the one-buffer discipline doyle's W1 review carried into this milestone). SIZE IS THE OPERATOR'S THREE-PART SHAPE (ruling 8, 2026-08-23, superseding the provisional refuse-arm): (1) content is CAPPED AT 500 UNICODE SCALAR VALUES at the CEREMONY SEAM ONLY (not a record property) — longer REFUSES with a named reason to the requester (`SEAL_CEREMONY_CONTENT_TOO_LONG`), never truncated; (2) the overlay SUPPORTS SCROLLING — a within-cap content can still overflow a small PTY grid, so the surface adapts and the content never shrinks; (3) SUBMIT IS NEVER GATED on scrolled-to-end. Ceremony content must be VALID UTF-8 (doyle pin under ruling 8): bytes that cannot be displayed verbatim are bytes nobody can consent to, so non-UTF-8 refuses with its own named reason (`SEAL_CEREMONY_CONTENT_NOT_UTF8`). Gate: doc — the CONTEXT.md ceremony clause's content-shown sentence; impl — the overlay rendering (content verbatim + subnet line + scroll window) and the seam's cap + UTF-8 refusals; unit — the overlay's emitted bytes contain the content verbatim and name the binding subnet, the rendered content is byte-identical to the buffer presented for mint, over-cap content refuses by name at the seam (a 500-scalar content passes, 501 refuses — counted in scalars not bytes), non-UTF-8 content refuses by its own name, the overlay windows over-surface content by scroll offset without shrinking it, and submit folds regardless of scroll position.
2026-09-06T13:09:02.3364491Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3364682Z 
2026-09-06T13:09:02.3364849Z ### REQ-SEAL-CEREMONY-ESC-CANCEL
2026-09-06T13:09:02.3368930Z - Title: ESC (OR CTRL-C) AT THE CEREMONY OVERLAY ABORTS CLEANLY — the releases#19 Esc-cancel ruling, which this requirement satisfies: NO partial seal record exists anywhere (mint_seal is simply never called for a cancelled ceremony — there is no staged record to roll back, by construction), the cancel SPENDS NOTHING on the attempt ledger (a cancel is not a presented guess: Presented::Absent, uncounted), a pending sealed-send is DROPPED, and the INITIATING AGENT receives a clear refusal naming the cancellation — never a silent hang or an ambiguous failure. The same clean-abort arm covers the involuntary endings: controller detach mid-ceremony and requester connection drop abort identically (no partial record, nothing spent, both sides answered). The key grammar is the SHARED fold_code_key kernel — one grammar for every ceremony surface (ADR-0052's overlay-pattern-shared sentence), so no ceremony can drift its own cancel rule. Gate: doc — the CONTEXT.md ceremony clause's Esc sentence; impl — the client cancel arm and the daemon abort arm; unit — Esc and ctrl-c fold to Cancel through the shared kernel, a cancelled ceremony produces no record and no ledger delta, the requester receives the named cancellation refusal, detach-mid-ceremony aborts identically.
2026-09-06T13:09:02.3372750Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3372920Z 
2026-09-06T13:09:02.3373278Z ### REQ-SEAL-NO-CEREMONY-SURFACE
2026-09-06T13:09:02.3377379Z - Title: A MINT REQUESTED WITH NO CEREMONY SURFACE REFUSES FAST AND BY NAME: no live session for the minter endpoint, no attached CONTROLLER on that session, or a controller that did not declare the seal-ceremony capability (an N-1 rc client) each refuse `SEAL_NO_CEREMONY_SURFACE` with a notice telling the agent to have the operator attach and retry (operator ruling 4, 2026-08-23). NO pending-mint state and NO timeout queue: the refusal is immediate and nothing is parked awaiting a controller. The controller-attached predicate reads the BROKER'S LIVE SEAT TABLE under the log lock (has_controller), NEVER the perch info stamp — the stamp lags the seat decision by documented hazard (KH 7.15 / info.rs terminal_normalize: a dead session's controller stamp survives until a daemon-side clear catches up, and SessionInfo's controller_by None is ambiguous between empty and local), so a stamp reader would run ceremonies against a corpse or refuse against a live seat. Gate: doc — the CONTEXT.md ceremony clause's no-surface sentence; impl — the seat-table check and the named refusal; unit — no-session refuses, session-without-controller refuses, non-capable controller refuses, each by name; an attached capable controller proceeds; and the predicate is the broker seat table, pinned so a perch-stamp rewrite cannot pass.
2026-09-06T13:09:02.3381320Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3381539Z 
2026-09-06T13:09:02.3381688Z ### REQ-SEAL-CEREMONY-RC-CLIENT
2026-09-06T13:09:02.3385908Z - Title: A REMOTE ATTACHED CONTROLLER RUNS THE CEREMONY CLIENT-SIDE AND SHIPS THE PROOF UP THE RC CHANNEL (CONTEXT.md ratified; the rc-paste client-originated precedent — the daemon has no reach into the operator's terminal or clipboard, so the overlay renders and the code is collected in the rc client process) — AND THE PROOF IS THE PRESENTED CODE, NEVER A CLIENT-SIDE VERDICT: verification, the attempt ledger, and the mint stay daemon-side (REQ-SEAL-CEREMONY-TOTP), because an agent on the box can speak the broker's IPC directly and a client-side decision would be decorative (the bring-up precedent, broker seat-resolver placement rationale). The ceremony records ride the EXISTING attach stream as additive serde variants, so the LOCAL and REMOTE arms are ONE code path — nothing about a remote controller is special except transport latency; N-1 clients are protected by the default-false capability declaration (REQ-SEAL-NO-CEREMONY-SURFACE refuses rather than pushing records an old client cannot decode). Gate: doc — the CONTEXT.md rc-arm sentence; impl — the ceremony AttachRecord variants and the client ceremony mode; unit — the upstream ceremony record carries the presented code and has no verdict field for a client to assert, keys fold through the shared kernel, and a ceremony round-trips over the attach machinery (push, code up, result down).
2026-09-06T13:09:02.3390088Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3390335Z 
2026-09-06T13:09:02.3390516Z ### REQ-SEAL-DESCRIBE
2026-09-06T13:09:02.3399850Z - Title: `spt api seal describe <token>` RENDERS THE SEAL RECORD'S FIELDS — token, content hash, fully-qualified minter, mint timestamp, ceremony kind — as the read surface for citing a seal WITHOUT re-presenting its content (the verify verb's complement: describe answers 'what does this token attest', verify answers 'is it THIS content'). Unknown token refuses `SEAL_UNKNOWN_TOKEN`; malformed token refuses at the format seam, distinct (REQ-SEAL-TOKEN-FORMAT). Read-only, no auth gate, answers on any member node of the binding subnet via the replicated store (REQ-SEAL-STORE-REPLICATES-SUBNET-SCOPED). Field rendering is LINE-ORIENTED KEY:VALUE on stdout so an agent citing a seal can quote it verbatim; the describe surface NEVER renders anything that is not in the record — in particular it cannot leak subnet secret material, because the record carries none (the minter address names a subnet; naming is not revealing). Gate: doc — the CONTEXT.md entry's describe clause; impl — the api verb + rendering; unit — field rendering exact over a known record, unknown-vs-malformed refusal distinction, exit codes. #219 (operator, FIELD-SEAL W2): DESCRIBE IS THE HUMAN SURFACE AND ITS BLOCK IS NOT A PARSE TARGET. The MINTER renders with its node half NAMED where this node can name it, in the house `LABEL (prefix…)` convention (REQ-PICKER-4's never-bare-hex rule) — `SPT_DEV:lia@HFENDULEAM (14efb80c…)`; MINTED_AT renders `YYYY-MM-DD HH:MM TZ` in the HOST NODE'S OWN timezone. Both renders are DERIVED — from this node's roster and gossiped registry, and from this node's clock — so TWO NODES MAY LEGITIMATELY RENDER ONE RECORD DIFFERENTLY and that difference is NOT a defect: the machine-readable answer is the record itself, never this text, and nothing derived here reaches verification (the fido2 tuple recomposes from the STORED spellings verbatim, REQ-SEAL-FIDO2-SIG-VERIFY's exactness rider, untouched). THE NAME RESOLVE IS A PREFIX MATCH AND ANSWERS ONLY WHEN UNAMBIGUOUS: a record stores its minter's node as an 8-CHAR KEY PREFIX (`registry::key_prefix` at the mint) while both label directories key on the FULL key, so the candidates are a SET, never a first match (roster_node_hex_resolver's doctrine) — EXACTLY ONE candidate key renders its name; zero (nothing known) and two-or-more (the prefix does not identify a node) BOTH degrade to the stored hex, because a name guessed from an ambiguous prefix attributes a decision seal to a machine the operator never named. An instant the local calendar cannot place degrades to the raw milliseconds rather than inventing a time. THE LABEL DIRECTORY IS AN INJECTED SEAM (the probe/decide split): the verb loads the union of the two existing tables at its edge and hands it to a pure renderer, so every arm of the naming rule unit-tests against a fixture table instead of against whatever the build box gossiped. Gate adds: unit — the naming rule's three arms (unique names, ambiguous degrades, unknown degrades), a minter that does not parse rendered untouched, the stamp's shape and its degrade-to-raw arm.
2026-09-06T13:09:02.3407939Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3408152Z 
2026-09-06T13:09:02.3408256Z ### REQ-SEAL-MINT-VERB
2026-09-06T13:09:02.3413425Z - Title: `spt seal mint` IS THE DECISION-SEAL ENTRY POINT: the text to seal arrives on STDIN, the verb requests the daemon's TOTP ceremony over exactly those bytes through the ONE production mint seam (Brain::seal_ceremony -> KIND_SEAL_CEREMONY; the CLI process never verifies, never ledgers, never mints - REQ-SEAL-CEREMONY-TOTP's write-through-daemon posture), and on admit prints the MINTED TOKEN to stdout as the whole result. STDIN IS TRIMMED EXACTLY AS `spt send` TRIMS ITS BODY (the REQ-HAZARD-ENVELOPE-CR-LINESAFE parity: a Windows `echo |` pipes a trailing CRLF, and a seal minted over an invisible trailing newline is a citation whose verify mysteriously reads NOT-BOUND) - so the buffer `spt seal mint` seals for a given text is byte-identical to the buffer a sealed send delivers for that same text; EMPTY content after the trim refuses fast CLI-side by name, before any ceremony opens (a ceremony over nothing asks the human to consent to nothing). REFUSALS RIDE THROUGH VERBATIM: no-surface, content cap, non-UTF-8, throttle, wrong-code and cancellation each surface the daemon's own outcome token and detail sentence - the CLI never re-words a refusal it did not decide (the W2 reply contract: every field is the broker's answer and none of it is the client's to summarize). The binding subnet resolves through REQ-SEAL-SUBNET-BINDING-DEFAULT (minter endpoint's anchor; `--subnet` overrides). EXIT CONTRACT, mirroring verify's 0-iff-BOUND: exit 0 IF AND ONLY IF the ceremony ADMITTED and the token printed; refused / throttled / cancelled / transport-error are each nonzero with the named reason on stderr. Gate: doc - the CONTEXT.md wax-seal entry's mint-verb sentence; impl - the CLI verb and its outcome mapping; unit - admit prints the bare token and exits 0, each non-admit outcome exits nonzero carrying the daemon's own detail verbatim, stdin trim parity with send, empty-content fast refusal by name.
2026-09-06T13:09:02.3418845Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3419120Z 
2026-09-06T13:09:02.3419277Z ### REQ-SEAL-SEND-SEALED
2026-09-06T13:09:02.3428343Z - Title: `spt send <id> --seal [--subnet <name>]` IS SEALED DISPATCH: one verb that runs the TOTP ceremony over the EXACT BYTES THAT WILL BE DELIVERED and, on admit, delivers the message carrying the minted token as its sender-authored envelope attribute (REQ-SEAL-ENVELOPE-ATTR). ONE BUFFER END TO END: the body read once at the verb's head (trimmed exactly as an unsealed send trims) is THE buffer the overlay shows, THE buffer the mint hashes, and THE buffer delivered - never re-read and never re-encoded between ceremony and delivery, so the receiver's `spt api seal verify` over the delivered body reads BOUND by construction; the unit pins ceremony-buffer == delivered-buffer identity. THE OVERLAY NAMES THE DESTINATION ID as well as the binding subnet (design of record, doyle W3 dispatch point 2 - the W2 overlay named only the subnet, so this is an overlay change too: the destination line renders when present, and the human consents to sealing THIS text TO THIS endpoint UNDER THIS subnet). A NON-ADMITTED CEREMONY SENDS NOTHING: refused, throttled and cancelled each surface the daemon's outcome verbatim and the message is neither delivered nor spooled (REQ-SEAL-CEREMONY-ESC-CANCEL's pending-sealed-send-DROPPED arm, which this verb finally gives a consumer). The binding subnet resolves through REQ-SEAL-SUBNET-BINDING-DEFAULT against the destination. Gate: doc - the CONTEXT.md sealed-message clause; impl - the --seal arm in cmd_send (ceremony before delivery, attr authoring at the compose seam); unit - ceremony-buffer == delivered-buffer identity, the overlay brief carries the destination id, a non-admit outcome delivers and spools nothing, an admit delivers carrying the minted token. #223 (FIELD-SEAL W2): THE SENDER'S ANSWER CARRIES THE MINTED TOKEN — `SENT:<target> seal=<token>` — because before this the ONLY sender-visible copy of the token was a ceremony-overlay line stranded in the sender's PTY, which no script or agent can consume (field-measured, v0.62.0 first sealed exchange). THE TOKEN RIDES EVERY OUTCOME LINE A SEALED SEND CAN ANSWER WITH — SENT, QUEUED, QUEUED(idle-only), DEFERRED (doyle ruling, 2026-08-25) — because the token is evidence of the MINT, not of delivery: the ceremony runs BEFORE any delivery arm by construction, so a send that only spooled still minted a record its sender may need to cite. The suffix appears IFF A MINT PRECEDED THE ANSWER: an unsealed send's answer is byte-identical to before, and refusal/non-admit paths return before any answer line exists, so no path can print an empty `seal=`. AND THE ADMITTED CEREMONY PRINTS NOTHING TO THE PTY: the rc takes the overlay down and emits NO notice on admit, since the token now reaches the sender on the answer line and that notice was the residue; every NON-admit outcome still renders the daemon's sentence verbatim, because nothing else surfaces a failure or a cancel. The remaining cosmetic half — the blanked overlay rows the session repaints only over time — is REQ-BROKER-SCREEN-GRID's scope, not this one's. Gate adds: unit — the answer composer over all four outcome labels, and the unsealed answer unchanged.
2026-09-06T13:09:02.3436920Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3437163Z 
2026-09-06T13:09:02.3437351Z ### REQ-SEAL-SUBNET-BINDING-DEFAULT
2026-09-06T13:09:02.3442256Z - Title: THE BINDING SUBNET OF A SEAL RESOLVES BY ONE DETERMINISTIC RULE (operator ruling 5 + Q8, doyle W3 dispatch point 3): (1) an explicit `--subnet <name>` OVERRIDES everything below - the caller named the key the human will present - but a name the MINTER endpoint is not a member of refuses fast by name (the ceremony could never verify a member-or-admin code of a subnet the minter has no seeds for); (2) otherwise the seal binds to the MINTER ENDPOINT'S ANCHOR SUBNET; (3) on sealed dispatch where the DESTINATION does not share the minter's anchor, the LEXICOGRAPHICALLY-FIRST subnet shared by both endpoints - deterministic, so one pair of endpoints always binds one subnet and the overlay's named subnet is reproducible run to run; (4) NO SHARED SUBNET refuses by name (`SEAL_NO_SHARED_SUBNET`) - NEVER fall back to a subnet the receiver cannot verify in, because the replication scope IS the verification audience: a seal whose binding subnet excludes the destination's nodes is a citation the recipient structurally cannot check (REQ-SEAL-STORE-REPLICATES-SUBNET-SCOPED). The resolver is a PURE FUNCTION over (override, minter anchor, minter's subnet set, destination's subnet set) so the whole rule is one unit table; the overlay always names the chosen subnet (W2 already renders it). Gate: doc - the CONTEXT.md binding-subnet clause; impl - the pure resolver and its callers (mint verb, sealed dispatch); unit - the rule table: anchor default, shared-lex-first when the destination lacks the anchor, override beats both, override-outside-minter-membership refuses by name, no-shared refuses SEAL_NO_SHARED_SUBNET, and input-order independence of the shared-set pick.
2026-09-06T13:09:02.3446885Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3447109Z 
2026-09-06T13:09:02.3447238Z ### REQ-SEAL-ENVELOPE-ATTR
2026-09-06T13:09:02.3452780Z - Title: A SEALED MESSAGE CARRIES ITS SEAL TOKEN AS A SENDER-AUTHORED ENVELOPE ATTRIBUTE seal="<token>" riding like `json=` (collision-proof by construction: the token lives inside ONE attribute value and can never forge `from`/`type`; its alphabet, REQ-SEAL-TOKEN-FORMAT's 30-glyph set, is a strict subset of the reemittable attr charset so the attr can never smuggle envelope structure). SENDER-AUTHORED IS A DIFFERENT CLASS FROM RECEIVER-COMPOSED, AND THE BOUNDARY IS LOAD-BEARING: the ingress strip (REQ-TRUST-WARNING-ENVELOPE's RECEIVER_COMPOSED_ATTRS class - `trust-warning`, `mnemonics-json`, CONTEXT.md:960) inertizes attributes the RECEIVER composes about a delivery; the seal attr is the envelope author's OWN field (like `type`, `from`, a notify's id) and MUST ride end-to-end intact through that strip - and MUST NEVER be added to the strip class, which would delete the sender's own evidence citation at every ingress. THE ATTR SURFACES IN THE RECEIVER'S EVENT ENVELOPE: the receiving agent sees seal="..." on the delivered <EVENT> and cites or verifies from there. KH 7.5 TRANSPORTED, STATED AS A PROHIBITION ON EVERY FUTURE CONSUMER: the attribute is a CITATION, never an authorization subject - no consumer may branch authority on its presence or value; only a BOUND `spt api seal verify` verdict over the delivered body is evidence, and a forged attr is harmless by construction because verify recomputes the hash. Gate: doc - the CONTEXT.md sealed-message clause's attribute sentence; impl - the attr const and its sender-side authoring at the compose seam; unit - a sealed envelope survives strip_receiver_composed_attrs with the attr intact while the receiver-composed class is still stripped beside it, the attr renders in the delivered EVENT envelope and round-trips the envelope codec, the attr value is the bare token; int - a sealed send delivered CROSS-NODE arrives with the attr intact and its body verifying BOUND against the replicated record on the receiving node (doyle W3 dispatch point 5b).
2026-09-06T13:09:02.3458575Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.3458827Z 
2026-09-06T13:09:02.3459031Z ### REQ-SEAL-AUTH-BACKEND-SEAM
2026-09-06T13:09:02.3469620Z - Title: PLATFORM-AUTHENTICATOR ACCESS IS ONE PER-OS SEAM — ensure-keypair / sign-payload / availability — AND THE SEAM IS THE CONTRACT (#217 anchor 3): the enrollment verb (REQ-SEAL-ENROLL-VERB) and W2's FIDO2 ceremony consume the SEAM, never a backend. AVAILABILITY answers whether a platform authenticator is present and usable for the invoking session — a probe that cannot answer reports unavailable WITH ITS NAMED REASON, never a verdict-shaped guess (the firewall doctrine: bounded probes feed a PURE DECIDER so every decision arm unit-tests hermetically without the OS — the probe/decide split at firewall.rs and the injectable-probe seam of resolve_transport are the two house exemplars). ENSURE-KEYPAIR is the idempotent get-or-create of this node's seal-authenticator keypair, user-verification-gated at creation by the backend (Windows Hello's own prompt); it returns the PUBLIC KEY plus the seam's BACKEND KIND token, and the private key NEVER crosses the seam — no export surface exists, signing is the only use. SIGN-PAYLOAD is the user-verification-gated detached signature over caller-presented bytes — W2's ceremony consumer; W1 lands it behind the seam with the stub/decider arms unit-held and NO production caller. BACKENDS: Windows Hello via the WinRT KEYCREDENTIALMANAGER surface (the `windows` crate, features Foundation + Security_Credentials + Storage_Streams, cfg(windows)-gated). The earlier windows-sys/raw-ncrypt spelling rested on a premise MEASURED FALSE (#222, doyle's 4-arm probe + todlando's independent repro, 2026-08-25: the Microsoft Passport/NGC KSP refuses EVERY plain key name with NTE_INVALID_PARAMETER on open AND create, so the NTE_BAD_KEYSET create cue is unreachable and that route can never enroll on any box). The house rule is therefore the MEASURED one, not the blanket one: windows-sys for Win32 surfaces, the `windows` crate ONLY where the surface is WinRT-only — KCM is the precedent (doyle ruling, 2026-08-25; the crate is already in the graph via netwatch and wmi, so the compile-weight objection is sunk cost). Linux is unchanged; Linux is a NAMED-REFUSAL STUB this milestone — every seam question answers that libfido2 is the named later backend, verbatim, because the honest stub refuses by name while a stub faking availability is a trap (fleet = Windows + one Linux box); any other OS refuses likewise. ENCODINGS: pubkey and signature cross every boundary as LOWERCASE HEX of the backend's raw bytes (the identity.rs / release-keys.json convention — no PEM, no base64, and NO RE-ENCODING BY US: the backend's NATIVE blob rides verbatim as hex, so for `hello-kcm-rs256` the raw bytes ARE the 294-byte DER SubjectPublicKeyInfo the KCM surface returns; `no DER` always meant we never CONVERT INTO DER, never that a backend may not hand us one), and BACKEND KIND is an open string token beside them (the ceremony_kind precedent) so a record names its algorithm family without any consumer parsing key innards; the Hello backend's key algorithm is whatever the platform mints (ES256/RS256 class, NOT ed25519) — the kind token is what keeps that honest, and the cross-node verifier dependency this implies is W2's flagged ruling, not W1's. Gate: doc — the published enrollment page's per-OS backend story; impl — the seam module, the Hello backend, the named-refusal stub; unit — stub arms refuse with the libfido2 name verbatim, pure-decider arms hermetic under injected probes, hex encoding roundtrip, backend-kind token stability (the Hello arm's real OS prompt is field-exercised, stated honestly, not CI-unit-held).
2026-09-06T13:09:02.3479524Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3479818Z 
2026-09-06T13:09:02.3479943Z ### REQ-SEAL-ENROLL-VERB
2026-09-06T13:09:02.3490832Z - Title: `spt seal enroll-authenticator [--subnet <name>]` ENROLLS THIS NODE'S PLATFORM AUTHENTICATOR INTO A SUBNET'S SECURITY MATERIAL, GATED BY THE SHIPPED TOTP CEREMONY (#217 anchor 1). ORDERED SEAM CHECKS, EACH REFUSING BY NAME BEFORE ANY CEREMONY OPENS (no human should present a code for an enrollment that cannot complete): (1) the binding subnet resolves by the house rule's applicable arms — explicit `--subnet` wins but a subnet this endpoint is not a member of refuses fast by name, otherwise the endpoint's anchor subnet, and no anchor refuses by name (REQ-SEAL-SUBNET-BINDING-DEFAULT arms 1-2; enrollment is node x subnet — no destination, so the shared-subnet arm does not exist here); (2) seam availability (REQ-SEAL-AUTH-BACKEND-SEAM) — including the Linux named stub — refuses verbatim; (3) ALREADY-ENROLLED (the node x subnet key holds a record) refuses by name, NAMING THE EXISTING RECORD'S PUBKEY AND ITS IMMUTABILITY: records are immutable v1 (intake ruling 6 transported), re-enrollment/revocation is STOP-AND-REFER, never a lane decision, so the verb neither overwrites nor duplicates. THEN ensure-keypair runs (the backend's own user-verification prompt), and the verb requests the daemon's TOTP ceremony carrying FACTS — the pubkey hex and backend kind — plus, at the code arm, the PRESENTED CODE, never a verdict (write-through-daemon verbatim: verification, throttle and the record write all daemon-side). The ceremony rides the SHIPPED machinery as an additive purpose (the attach-stream additive-variant discipline of REQ-SEAL-CEREMONY-RC-CLIENT), and the overlay shows an ENROLLMENT BRIEF composed DAEMON-SIDE from the request's facts — naming the enrolling node's key short hex, the binding subnet, the backend kind and the enrolled pubkey's SHA-256 FINGERPRINT (64 hex via the one hash helper; the FULL pubkey hex lives in the record — an RSA public blob is ~588 hex chars (294-byte DER SPKI), measured, which no human can compare, and a fingerprint is what consent can actually read) — shown verbatim as ceremony content (the content-shown discipline: the human consents to THIS key for THIS node on THIS subnet), with the brief's own text carrying the ENROLL framing so even a chrome that says seal cannot misrepresent what is consented. THE THROTTLE IS THE EXISTING trust/seal-ceremony-gate.json LEDGER — enrollment mints NO new ledger (the denial-of-governance separation holds; a ruling is the only path to one) — throttle-before-verify, member-or-admin of the binding subnet, both seeds combined bitwise, cancel spends nothing: all inherited by riding the one ceremony-code path. ON ADMIT the daemon writes exactly one enrollment record (REQ-SEAL-ENROLL-RECORD-SUBNET-MATERIAL) under the same apply serialization as seal records and replies the record's fields; the verb prints them line-oriented. EXIT 0 IFF ENROLLED; every non-admit outcome (no-surface, throttled, refused, cancelled, ledger-unwritable) is nonzero carrying the daemon's own token and detail VERBATIM — the CLI never re-words a refusal it did not decide. Gate: doc — the published verb page; impl — the SealVerb variant + verb fn, the daemon enrollment-ceremony purpose arm + record write; unit — subnet-resolution arms (override wins, non-member override refuses by name, anchor default, no-anchor refuses), seam-unavailable refuses before ceremony, already-enrolled refuses before ceremony naming the record, admit writes exactly one record and exits 0, each non-admit outcome nonzero with the daemon detail verbatim, the daemon-composed brief carries node short hex + subnet + backend kind + pubkey hex and the ENROLL framing.
2026-09-06T13:09:02.3500858Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3501087Z 
2026-09-06T13:09:02.3501291Z ### REQ-SEAL-ENROLL-RECORD-SUBNET-MATERIAL
2026-09-06T13:09:02.3510246Z - Title: AN ENROLLMENT RECORD IS DURABLE SUBNET SECURITY MATERIAL, MEMBER-VERIFIABLE BY CONSTRUCTION (#217 anchor 2): `{pubkey_hex, node, subnet, enrolled_at, backend_kind}` — pubkey as lowercase hex of the backend's raw public key bytes, node as THE DAEMON'S OWN NODE-KEY SHORT HEX (the nodeid::load_or_create + registry::key_prefix helper pair, 8 hex chars, the ADR-0054 roster short form — NEVER the hostname, the W2 gate ruling transported verbatim), the subnet name EXPLICIT (unlike a seal record, an enrollment has no minter address to derive scope from — the field is the scope), the mint timestamp in epoch ms, and the backend kind token (REQ-SEAL-AUTH-BACKEND-SEAM's encoding clause). THE STORE IS `trust/enrollments.json` BESIDE THE SEAL STORE — one JSON document, atomic write, a NEW path fn in perch.rs per REQ-HAZARD-SINGLE-PATH-SOURCE, absent/corrupt LOADS EMPTY (the evidence-store degrade polarity: a husk can only LOSE the ability to prove enrollment, never mint false evidence). RECORDS RIDE THE SAME SUBNET-SCOPED REPLICATION FAMILY AS SEAL RECORDS (the dispatch's same-family clause): an ADDITIVE KIND-TAGGED wire record in the Seal stream family's feed (the sealmsg NDJSON discipline — a tagged record an N-1 daemon does not know DROPS CLEANLY per line, never lands as chat, never fails the feed), the emitter re-presenting every enrollment record of the subnet beside its seal records (duplicates free under the join), the pump worker unchanged in cadence. THE APPLY POINT IS INSERT-IF-ABSENT KEYED BY (node, subnet) — records commute, replay no-ops, and the store is a join-semilattice like the seal store; a same-key-different-pubkey arrival is DROPPED WITH THE EXISTING RECORD KEPT and counted as an anomaly, never merged: FIRST-ENROLLED WINS EVERYWHERE, because records are immutable v1 (intake ruling 6) and a conflicting claim for an enrolled slot is either a partition echo or adversarial, and the adversarial copy is harmless only if it can never displace the enrolled key. THE APPLY GATE admits a row only when THIS node is a member of the record's subnet AND the handshake-proven origin node is on that subnet's roster (fail-closed, the REQ-SEAL-STORE-REPLICATES-SUBNET-SCOPED posture verbatim, same NotifPolicy plumbing). Immutable once minted, no expiry and no revocation v1 — a compromised or replaced authenticator is the stop-and-refer case by design. Gate: doc — the published records surface; impl — record struct + store + path fn + wire kind + gate/emit arms; unit — roundtrip persistence, insert-if-absent join (replay no-op, collision-drop-keeps-existing counted as anomaly), gate verdict arms (admitted / non-member-subnet dropped / untrusted-origin dropped), husk-loads-empty, node half is the 8-hex short form never a hostname. int at gate: a record enrolled on one member node readable on another.
2026-09-06T13:09:02.3517977Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.3518225Z 
2026-09-06T13:09:02.3518359Z ### REQ-SEAL-CEREMONY-FIDO2
2026-09-06T13:09:02.3526682Z - Title: AN ENROLLED (node x subnet) OFFERS THE FIDO2 CEREMONY ON THE MINT PATH (#217 anchor 4): when the daemon's OWN node-key short hex x the binding subnet holds an enrollment record, the ceremony open carries the FIDO2 offer plus the PAYLOAD TO SIGN, composed DAEMON-SIDE with minted_at FIXED BEFORE the request ships — the canonical binding tuple, registry-pinned to the exact spelling `spt-seal-fido2-v1\n{content_hash}\n{minter}\n{minted_at}\n` (versioned domain-separation prefix; UTF-8; trailing newline; field order pinned) — so every FIDO2 record is SELF-AUTHENTICATING from its own fields on any member node (signing content alone would leave minter/time unbound and a rostered rogue node could fabricate a record around a replayed signature). NO signer_node FIELD EXISTS; THE SIGNER IS THE RECORD'S MINTER NODE, and a future signer_node is ADDITIVE-OPTIONAL DEFAULTING TO IT (doyle ruling 2026-08-24: the v1 records are forward-valid by this title — a cross-node signer is a field addition behind a ruling, never a migration). THE REMOTE DEGRADE IS AN OFFER WITHDRAWAL, NOT A FAILURE: the open ships the enrolled node's short hex beside the offer, the client compares it against ITS OWN node and on mismatch renders the ORDINARY TOTP overlay with ZERO error chrome — no doomed sign attempt, nothing in logs or telemetry wearing a defect's costume (doyle rider 1). The client's Hello prompt IS the human-presence ceremony — the seam's sign-payload gains its first production caller — and the client ships the SIGNATURE PROOF, never a verdict: the daemon VERIFIES the proof against the enrolled pubkey (backend-kind dispatch) BEFORE minting, and mints ceremony_kind `fido2` with the additive optional signature_hex (serde default + skip_serializing_if: old records byte-identical, N-1 daemons ignore the unknown field on the seal feed). FAILED-PROOF REFUSALS SPEND NOTHING on the ceremony-gate ledger — a bad signature is not a guess at the shared secret, and charging it would let a broken client shut the gate a human needs (the denial-of-governance direction) — and the overlay FALLS BACK to TOTP code entry on the SAME overlay; a HELLO CANCEL is a fallback-to-TOTP, DISTINGUISHABLE from Esc, which still aborts everything with the TOTP cancel clauses verbatim (nothing spent, nothing minted, requester answered). Gate: doc — the published ceremony page's FIDO2 clause; impl — the offer decision + payload composer + proof-verify + fido2 mint arm; unit — tuple spelling pinned (prefix, field order, trailing newline), offer iff enrolled, proof-verify admits a valid signature and refuses a wrong one without charging the ledger, Hello-cancel falls back while Esc aborts, minted record carries kind fido2 + the signature.
2026-09-06T13:09:02.3534356Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3534531Z 
2026-09-06T13:09:02.3534745Z ### REQ-SEAL-FIDO2-RC-CLIENT
2026-09-06T13:09:02.3540217Z - Title: THE FIDO2 CEREMONY RIDES THE EXISTING ATTACH RECORDS AS ADDITIVE FIELDS, NOT NEW VARIANTS (doyle wire ruling 3, 2026-08-24 — the measured N-1 posture supersedes the variants wording): the ceremony OPEN gains optional offer/payload fields and the upstream CODE record gains optional proof fields, so an N-1 CONTROLLER IGNORES THE UNKNOWN FIELDS AND DEGRADES TO THE PLAIN TOTP OVERLAY — the universal fallback keeps working, nothing hangs, no undecodable record, no refusal even needed; the existing default-false seal_ceremony capability bit keeps gating the ceremony surface itself, and no new bit is minted for the offers. SIGNING IS CLIENT-SIDE WHERE THE HUMAN IS (the rc-paste precedent): the rc client calls the authenticator seam's sign-payload over the daemon-composed tuple bytes and ships the SIGNATURE up the attach wire — a proof, never a verdict; verification, the ledger, and the mint stay daemon-side (an agent on the box can speak the broker's IPC directly, so a client-side decision would be decorative). LOCAL AND REMOTE CONTROLLERS ARE ONE CODE PATH — nothing about remote is special except transport latency, and a remote controller whose node lacks the enrolled key degrades to TOTP through the same seam-refusal fallback every client uses. An old DAEMON never emits the offers, so a new client never sends an unexpected proof to it. Gate: doc — the published page's remote-controller sentence; impl — the additive open/code fields + the client sign arm + the overlay fallback; unit — the open and code records round-trip with and without the new fields (absent fields decode as the plain TOTP shapes — the N-1 arm), the upstream proof record carries signature bytes and has no verdict field, and the client's seam-refusal path folds to TOTP entry.
2026-09-06T13:09:02.3545268Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3545486Z 
2026-09-06T13:09:02.3545614Z ### REQ-SEAL-ENROLL-SHORTCUT-E
2026-09-06T13:09:02.3550225Z - Title: `E` ON THE TOTP OVERLAY IS ENROLL-AND-SUBMIT, OFFERED IFF (node x subnet) IS NOT ENROLLED (#217 anchor 5): the daemon stamps the offer on the ceremony open (an additive field — an enrolled pair's overlay NEVER shows E, and an N-1 client ignores the field and simply shows the plain overlay); pressing E runs the client-side seam's ensure-keypair (Hello's own prompt) and ships ONE presented code together with the enrollment FACTS (pubkey hex + backend kind) on the upstream code record's additive fields. ON ADMIT THE DAEMON PERFORMS BOTH-OR-NEITHER UNDER THE ONE APPLY LOCK (doyle wire ruling 3): the enrollment record writes AND the pending seal mints, or NEITHER — a failed enrollment mints nothing and a failed mint enrolls nothing, both halves unit-pinned — so one human code entry yields a consistent pair and never a half-state. Every TOTP clause carries unchanged: throttle-before-verify on the same ledger, wrong code counts and re-prompts, Esc cancels everything spending nothing. An old daemon never offers E, so its clients never send enrollment facts it cannot read. Gate: doc — the published page's E clause; impl — the offer stamp + the E client arm + the both-or-neither admit arm; unit — offer iff not enrolled, enrolled overlays never offered, admitted E writes exactly one enrollment and one seal under the lock, the failed-enrollment arm mints nothing, the failed-mint arm enrolls nothing, cancel spends nothing.
2026-09-06T13:09:02.3554283Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3554494Z 
2026-09-06T13:09:02.3554654Z ### REQ-SEAL-FIDO2-SIG-VERIFY
2026-09-06T13:09:02.3562889Z - Title: `spt api seal verify` EXTENDS TO THE SIGNATURE FOR fido2 RECORDS, ON ANY MEMBER NODE (#217 anchor 6): when the record's ceremony_kind is `fido2`, BOUND requires the content hash to match AND the signature to verify — the verifier RECOMPOSES the canonical tuple FROM THE RECORD'S OWN STORED FIELD SPELLINGS VERBATIM (the content_hash hex as stored, the fully-qualified minter as stored, minted_at as stored — no re-derivation, no respelling; ANY divergence is NOT-BOUND, never a second canonical form: doyle's exactness rider) and checks signature_hex against the pubkey looked up STRICTLY BY (node, subnet) — the record minter's node half x its binding subnet — in the replicated enrollment store, REFUSING BY NAME when no enrollment record exists there (no fallback to some record with this pubkey elsewhere: the replication scope IS the verification audience, the SEAL_NO_SHARED_SUBNET rationale — doyle ruling 3, 2026-08-24, unit-pinned). ALGORITHM DISPATCH RIDES THE ENROLLMENT RECORD'S backend_kind TOKEN: `hello-kcm-rs256` = RSASSA-PKCS1-v1_5 over SHA-256 of the tuple bytes, the pubkey parsed from the stored DER SubjectPublicKeyInfo, verified by the pure-Rust rsa crate — VERIFY-ONLY IN CORE (signature creation stays behind the authenticator seam; RUSTSEC-2023-0071 is private-key-op timing and cannot reach a verify-only consumer — the dep comment states exactly that with the advisory id, per doyle ruling 1) — and a FUTURE BACKEND ADDS A TOKEN AND A VERIFIER ARM, NEVER A RECORD MIGRATION; an unknown token refuses by name. `hello-rs256` IS RETIRED UNMINTED (#222 ruling, 2026-08-25) and gets NO verifier arm: its mint path was structurally unreachable on every box, so no record can carry it by construction (census: trust/seals.json holds totp records only, no enrollment record exists anywhere) — a record claiming that token is fabricated, and the unknown-token refusal is the honest answer to it. A fido2 record with a MISSING signature is NOT-BOUND (a self-declared fido2 mint that cannot be checked is not evidence); non-fido2 records verify exactly as before — the signature path is additive. `describe` renders the ceremony kind and the SIGNATURE PRESENCE (the fingerprint discipline anywhere a human reads key material). Exit contract unchanged: 0 IFF BOUND. Gate: doc — the published verify clause; impl — the signature arm + tuple recomposition + strict lookup + rsa verify + describe rendering; unit — valid signature BOUND, wrong signature NOT-BOUND, tuple recomposition pinned to stored spellings (a respelled minted_at diverges to NOT-BOUND), missing-enrollment refusal by name distinct from NOT-BOUND, unknown backend token refusal, fido2-with-missing-signature NOT-BOUND, non-fido2 records unaffected.
2026-09-06T13:09:02.3570644Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3570859Z 
2026-09-06T13:09:02.3571035Z ### REQ-PSYCHE-INGEST-SERIALIZED
2026-09-06T13:09:02.3580170Z - Title: EVERY WRITE INTO A SHARED CONTEXT-STORE WORKTREE IS SERIALIZED BY AN EXCLUSIVE ADVISORY LOCK ON THAT WORKTREE, AND A LOSER WAITS RATHER THAN FAILING (releases#221): concurrent per-agent project-tier ingests commit into ONE shared `p-<project_id>` worktree and race git's `index.lock` and the branch's HEAD ref -- 6 PSYCHE_INGEST_FAIL across 3 agents measured on HFENDULEAM 2026-08-25, `fatal: Unable to create '.../worktrees/<name>/index.lock': File exists` and `cannot lock ref 'HEAD': is at X but expected Y`. THE LOCK LIVES AT THE LEAF GIT OPS OF `BranchStore` -- `commit_in_worktree`, `fast_forward_worktree`, `merge_commit_in_worktree` -- NOT at the ingest call site (doyle ruling 2): the ingest path is one of SIX writers of the same worktree (spt-live reconcile, syncmerge fast-forward + merge, contextstore conflict/rename/monic arms), several of them in OTHER PROCESSES, so a narrow guard leaves the identical collision reachable through a rarer door. It is an EXCLUSIVE FILE LOCK ON A STABLE NEVER-RENAMED SENTINEL keyed by the WORKTREE PATH (the `worker_seq` precedent: fs2, RAII-released on handle drop or process death, so a crashed holder strands no file and there is no stale-lock class to sweep) -- NEVER git's own `index.lock`, which stays exactly what `sweep_stale_index_locks` treats it as. THE LOCK IS NON-REENTRANT AND ACQUIRED ONLY AT THE LEAF, so a caller walking many worktrees (the rename loop) acquires them ONE AT A TIME and never nests -- the no-lock-ordering claim is an ASSERTION IN A UNIT, not a comment (doyle ruling 2 rider). ACQUISITION BLOCKS WITH A BOUND (doyle ruling 3): default 10s (two pulse periods; the guarded op is sub-second, so contention is milliseconds), overridable ONLY by a test-scoped `SPT_TEST_*` env var that never enters the operator vocabulary. A pulse tick MAY block -- it blocks only that endpoint's own driver thread, and failing fast is the very defect. ON TIMEOUT the acquire returns an ERROR carrying THE SENTINEL PATH AND THE ELAPSED WAIT BESIDE THE BOUND (at-budget vs under-budget is the only discriminator between a deadlock and a slow box), and that error rides the EXISTING capture-and-continue seam: PSYCHE_INGEST_FAIL is printed, a hard-ingest strike accrues, and THE DROP FILE IS PRESERVED -- nothing is consumed, so nothing is lost, and the F-032 preserve-pending arm (REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE) is untouched beneath it. THE LOCK IS THE ONLY MECHANISM: no retry belt over git's own lock errors (doyle ruling 4) -- a second handler would make a broken lock invisible, and an index.lock failure observed WHILE the sentinel is held is a FINDING to file, never a transient to swallow. Sibling surface: REQ-PSYCHE-INGEST-FAILURE-LOUD (the misplaced-drop half of the same observability seam). Gate: impl -- the sentinel path fn + bounded exclusive acquire + its wiring at the three leaf ops; unit -- an acquire held by one handle makes a second measure elapsed >= the hold and THEN succeed (the lock is load-bearing, proven by elapsed not by a rate), the timeout error names the sentinel path AND the elapsed AND the bound, the bound is env-overridable in test scope only, a multi-worktree caller's acquisitions are sequential and never nested (the lock-ordering assertion), and a timed-out ingest DELETES NO DROP. int -- TWO PROCESSES committing concurrently into one shared worktree: zero failures, every slice PRESENT by existence read, per-arm durations reported, plus a DETERMINISTIC arm where process A holds the sentinel for a known T and process B's acquire measures elapsed >= T then succeeds.
2026-09-06T13:09:02.3589793Z - Required stages: impl, unit, int
2026-09-06T13:09:02.3590010Z 
2026-09-06T13:09:02.3590156Z ### REQ-PSYCHE-INGEST-FAULT-BRIEF-WARN
2026-09-06T13:09:02.3594703Z - Title: A RESUME BRIEF COMPOSED WHILE AN INGEST FAULT STANDS SAYS SO, IN THE BRIEF (releases#221 leg 2): a terminal ingest failure already LATCHES per-kind (`PsycheBudgets.latch_ingest`, hard budget 3) and already stamps `psyche_host_error`, so the fact is PULLABLE from `endpoint list` -- but it is PUSHED NOWHERE, and the agent whose durable tier just failed to take its commune is precisely the agent about to be rebuilt from the PREVIOUS one, confidently and wrong. The brief gains ONE WARNING LINE beside the existing staleness sentence and in the same voice (the REQ-PSYCHE-INGEST-FAILURE-LOUD invariant-2 family in `resume.rs`) -- reading it IS the recovery, so it names what is degraded and what the reader should distrust. THE TRIGGER IS ANY STANDING INGEST LATCH AT BRIEF-COMPOSE TIME, not only the terminal strike (doyle ruling 5): the latch IS the standing-fault vocabulary, and a brief composed under a standing fault is exactly the degraded read the ticket is about. NO DURABLE MESSAGE IN V1 -- the brief reaches the agent at the ONE moment the fact is actionable, while a message would also reach it mid-work where it can do nothing. A clean latch set adds NOTHING to the brief: the healthy brief's bytes are unchanged, so the warning's presence is the signal and its absence is not a claim of health by omission. Gate: impl -- the latch read at compose + the warning line; unit -- a brief composed with a standing ingest latch carries the line and names the fault, the SAME brief with no standing latch is byte-identical to the pre-change shape (the vacuity guard: a line that always renders proves nothing), and a standing TURN latch alone does NOT emit the ingest warning (the per-kind separation BAROMETER W1 built must not be undone by the reader).
2026-09-06T13:09:02.3599767Z - Required stages: impl, unit
2026-09-06T13:09:02.3599992Z 
2026-09-06T13:09:02.3600240Z ### REQ-ENDPOINT-DROP-DIR-SURFACE
2026-09-06T13:09:02.3606366Z - Title: THE REGISTERED COMMUNE DROP DIR IS READABLE FOR THIS NODE'S OTHER ENDPOINTS, NOT ONLY FOR SELF (releases#224, scope amended at measure-first): `endpoint list --json` HAS carried `drop_dir` on the SELF pin since 2026-08-01 (REQ-PSYCHE-INGEST-FAILURE-LOUD, `registered_drop_dir` resolving through the SAME `spt_live::resolve_endpoint_drop_dir` the daemon ingest anchors on -- one anchor, so the read surface cannot drift from the watcher), and the ticket's premise that no read verb exposes it is FALSE at head and at the shipped 0.62.0 binary; the residual is that the field appears EXACTLY ONCE in the whole roster JSON. The field census's real question is about ANOTHER agent's endpoint -- an orphaned drop is diagnosed by whoever finds it, not by its owner -- so `LocalPerchJson` rows gain `drop_dir` on the SAME terms as their `activity` and `host_error` siblings: read from that perch's own `info.json`, LOCAL ROWS ONLY, additive and `skip_serializing_if` so an endpoint with no recorded cwd/adapter or a manifest declaring no commune dir OMITS the key rather than guessing (an absent field is honest; a guessed dir is worse than silence). SUBNET-REMOTE ROWS (`EndpointRowJson`) STAY BARE (doyle ruling 1): a remote endpoint's drop dir is another NODE'S filesystem fact, and surfacing it here invites acting on a path this machine cannot read. The HUMAN view gains the same fact for SELF -- one annotation line under the self pin, the `psyche-host:` / `input-translation:` family -- so the answer does not require `--json`. Gate: doc -- the published drop-file-protocol clause telling an agent to READ the resolved dir rather than re-derive it, naming both JSON fields and the human line (the DISCOVERABILITY half of the residual: nothing pointed at the shipped field, which is how a multi-round probe happened over a fact that was already there, and how the ticket came to be filed on a false premise); impl -- the local-row field + the human self-pin line, both sourced through the one resolver, never a second derivation; unit -- a local row with a resolvable dir CARRIES it while a row with no cwd/adapter OMITS the key (the pair asserted together, since either alone passes an implementation that is uniform the wrong way), the remote row shape is UNCHANGED (the ruled boundary, pinned so a later widening is a decision and not a drift), and the human self pin renders the dir when known and renders NOTHING extra when not.
2026-09-06T13:09:02.3613171Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3613353Z 
2026-09-06T13:09:02.3613557Z ### REQ-EVENT-ATTR-PASSTHROUGH
2026-09-06T13:09:02.3619605Z - Title: THE EVENT ENVELOPE'S SENDER-AUTHORED ATTRIBUTE SET IS OPEN, AND THE PUBLISHED CONTRACT STATES THE CLASS RULE, NOT ONLY PER-ATTR OBLIGATIONS: any consumer that re-renders deliveries (adapter pipeline, digest, relay surface) MUST pass through every envelope attribute it does not itself consume — re-emitting from a fixed allowlist of known names silently deletes every attribute added after the list was written (measured 3x in the field; 3rd recurrence was an adapter render keeping only from+body, deleting the seal citation), and the deletion is invisible at the dropping site because nothing there names what it dropped. The ONLY closed list is the receiver-composed STRIP class (trust-warning, mnemonics-json) — a strip list, never a render list. MIRROR SAFETY CLAUSE: a passed-through value stays IN ATTRIBUTE POSITION with its wire escaping intact — unescaping an attribute value into body/frame context hands a hostile sender a forgery seam (a crafted value closes the re-rendered tag and lands text at the consumer's frame level); decode only where the final consumer parses attributes. CONSUME IS DEFINED, NOT LEFT TO THE READER (perri's discriminating question, 2026-08-25): a pipeline consumes an attribute when its handling RE-EXPRESSES the value's distinction to the agent (content re-rendered into its own surface, or a dispatch whose agent-visible outcome differs per value); reading-then-emitting-shared-output is peeking, and the DROP TEST decides — if omitting the attr makes two sender-distinct deliveries indistinguishable at the agent surface, it rides. NAMES ARE TOKENS: grammar [a-z0-9_-]+, hostile-reachable (envelopes arrive from peer NODES), pass through only a grammar-matching name, refuse/drop loudly anything else in tag position. Gate: doc — the overview.md wire-contract clause stating the pass-through obligation, the consume definition + drop test, the name-token grammar rule, and the escaping-position rule.
2026-09-06T13:09:02.3625091Z - Required stages: doc
2026-09-06T13:09:02.3625249Z 
2026-09-06T13:09:02.3625425Z ### REQ-UPDATE-COMPOSITE-EXIT-PRECEDENCE
2026-09-06T13:09:02.3634234Z - Title: THE COMPOSITE UPDATE'S EXIT IS FOLDED BY PRECEDENCE, NEVER BY WRITE ORDER (releases#216): `cmd_update_composite` aggregated leg exits with `worst = code` — the LAST nonzero, not the worst — so under `--restart` (the only plan where a leg runs AFTER the isolated adapters leg) an Adapters FAILURE of 1 followed by a Finish REFUSAL of 3 reported 3, and a refusal MASKED a failure. That inverts REQ-UPDATE-REFUSAL-EXIT-DISTINCT's contract, whose 3 means refusal present AND no failure — the same class of misread as the releases#153 field report where a refused fleet roll recorded itself as rolled. THE TOTAL ORDER IS 0 < 3 < EVERY OTHER NONZERO, classed BY VALUE and never by which constant minted it (the tree holds TWO distinct 3s — `EXIT_NOT_ELEVATED` and `EXIT_REFUSED_NO_WORK` — and a fold that cared which one it saw would grow a second vocabulary to drift from). An UNKNOWN nonzero is FAILURE, not refusal (doyle ruling 1): mapping an unminted code to the polite class would re-create this very defect for a code nobody has designed yet, and clap's usage `2` therefore sits in the failure class too, which is what makes the fold TOTAL. WITHIN A CLASS THE FIRST-SEEN CODE SURVIVES (doyle ruling 2) — the earliest failure is the one whose diagnostics the operator has already read, and later-overwrites-earlier is the defect mechanism itself. THE AGGREGATION CONTRACT IS STATED WHERE BOTH AUDIENCES READ IT: in the function's own doc (it stated the abort-vs-isolate failure policy and said NOTHING about aggregation, which is how the face stayed invisible to a source reader) AND on the published self-update page, because callers SCRIPT on these codes and a consumer misreading this surface is the documented history. SCOPE DECLARED, NOT ASSUMED (doyle ruling 3): this requirement covers the FOLD and the LEG ORDER that makes the defect reachable. That the composite PLUMBING routes each real leg's exit into the fold against the real binary stays UNPROVEN by this lane — an end-to-end arm would have to manufacture an adapters failure and a finish refusal, and what it would add beyond the units is that two legs still return codes their own suites already pin; the gate reads the single fold call-site as the review-arm for it instead. Gate: doc — the published self-update clause naming the precedence and both classes; impl — the pure fold + its ONE call site in the composite loop; unit — BOTH DIRECTIONS asserted together (failure-then-refusal folds to the FAILURE and refusal-then-failure folds to the same failure, since a fold that merely reversed the write order passes either arm alone), an unknown nonzero outranking a refusal, first-seen surviving within the failure class, success never outranking anything, and the leg plan pinning that Adapters PRECEDES Finish under `--restart` (the ordering the defect needs — a later plan change would silently un-reach it and the fold's arms would keep passing).
2026-09-06T13:09:02.3642256Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3642538Z 
2026-09-06T13:09:02.3642675Z ### REQ-ACL-LOCAL-SUBJECT-ONE-SHAPE
2026-09-06T13:09:02.3654329Z - Title: EVERY LOCAL FEEDER OF THE ADMISSION GATE HANDS IT A PROVEN ENDPOINT ID OR NOTHING — ONE MEANING, ONE SPELLING (releases#215): `gate::admit_local_delivery`'s `sender` is a SUBJECT (`Subject::SenderEndpoint` matches it by equality), and its three local feeders disagreed on what they put there — `spt send` passed the session-proven bare id, `spt ring` passed `resolve_from(--from)` (where an EXPLICIT `--from` BEATS session detection), and `notify` passed `NotifRow.from_id`, the COMPOSITE `{issuer}@{node_hex}`. A tier-1 rule an operator wrote for endpoint `ling`, that the CLI accepted and the view renders, therefore covered two of three local delivery verbs and COULD NEVER MATCH ON NOTIFY: it failed in the SILENT direction. `access_check_with_sender`'s own doc already forbade exactly this (NEVER pass `WanMessage.from` here -- an explicit `--from` beats session detection in `resolve_from`, so it would admit a forged subject), as does REQ-ACL-SUBJECT-CHAIN's ratified title — the code contradicted three landed documents. NOT FILED AS A SECURITY CLAIM, deliberately: REQ-ER-INBOUND-LOCK-ALL-PATHS already publishes same-user local code as outside what any spt gate holds, so a forged-subject framing would overclaim against a boundary we published ourselves. THE REMEDY IS ONE DERIVATION SITE, NOT THREE CORRECTIONS: `session_proven_sender` is threaded IN from the CLI (`ring::ring` gains a `sender_proven` parameter in `cmd_send`'s shape) rather than re-derived inside each verb, and `cmd_notify`'s INLINE RE-SPELLING of `resolve_from`'s body — the drift mechanism that let two sites diverge with neither looking wrong on its face — is replaced by a call to it. NOTIFY PASSES `None` AND TIER 1 ABSTAINS (doyle ruling 6): the chain falls to the node tier, byte-for-byte the documented behaviour for a sender who proved nothing. Splitting the composite at `@` is REFUSED as a fix — deriving a subject from a DISPLAY string is the same defect one layer down, and `from_id` legitimately carries the composite FOR RENDERING, which a fix blaming it would break. THE SMALLER PROMISE IS RECORDED, NOT SWALLOWED: a tier-1 rule about `ling` still does not MATCH on notify — it abstains BY DESIGN — and carrying a proven-sender field on the row so it could match is tracked as a separate EVAL item, never as delivered here. THE AUTHORING-TIME REFUSAL GAINS THE OTHER HALF OF THE SAME CONFUSION (doyle ruling 5): `build_access_rule` already refuses a sender-endpoint rule on a surface carrying no proven sender (`ACCESS_DEAD_SUBJECT`, REQ-ACL-SURFACE-ATTRIBUTABILITY) — it now also refuses a COMPOSITE typed into the bare-id slot (`--endpoint ling@<node>`), which can never equal a proven bare id, in that arm's existing vocabulary. A rule that can never match must refuse at authoring time whichever half of the confusion produced it. SIDE EFFECT PINNED, NOT LEFT INCIDENTAL (doyle ruling 9): `admit_local_delivery` records the stateful-firewall reply window with `note_outbound(sender, …)` — the SAME string — so a caller-chosen id could open a reply window it never earned; one argument feeds both, so normalizing the feeders closes it BY CONSTRUCTION and a unit asserts that rather than leaving it to be re-derived. That closure is reported on the ticket carrying its own limit verbatim: its consumers were read, never measured. Gate: impl — the threaded `sender_proven` parameter + the notify `None` + the `cmd_notify` call to `resolve_from` + the composite-subject refusal; unit — the three feeders asserted TOGETHER (send and ring hand a proven bare id or None; ring's gate subject is UNCHANGED by an explicit `--from`, the arm that fails on the pre-fix spelling; notify hands None so tier 1 abstains rather than judging a display string), the composite-into-bare-slot refusal named and distinct from `ACCESS_DEAD_SUBJECT`'s surface arm, and the `note_outbound` window opening only for a proven subject.
2026-09-06T13:09:02.3664789Z - Required stages: impl, unit
2026-09-06T13:09:02.3664990Z 
2026-09-06T13:09:02.3665171Z ### REQ-IO-EVENT-BUS
2026-09-06T13:09:02.3668367Z - Title: THE IO EVENT FUNNEL IS CONSUMER-AGNOSTIC INTERNALLY: emitters publish to ONE bus and the bus fans out to registered sinks (emitters -> bus -> {shell-link, core consumers}), so the adapter-consumption work tracked as releases#234 lands as a NEW READER registering a sink and never as a funnel rework. No emitter names a consumer and no consumer is reachable except through the bus; a sink list with exactly one entry today is the point, because the second entry must cost a registration rather than a refactor. ONE SINK FAILURE NEVER SUPPRESSES ANOTHER: publish returns a per-sink outcome and a failing sink is reported and stepped over, never propagated as a publish failure — an IO event is an OBSERVATION, and an observation that aborts the operation it observes has become a control path, which is the defect class this funnel exists to avoid. Emission is best-effort against the emitting operation: no `api state` call, no send, and no delivery may fail, block, or change its exit because a sink failed.
2026-09-06T13:09:02.3671251Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3671527Z 
2026-09-06T13:09:02.3671680Z ### REQ-IO-EVENT-TAXONOMY
2026-09-06T13:09:02.3675142Z - Title: THE IO EVENT VOCABULARY IS CLOSED AND NAMED IN ONE PLACE: USER_INPUT, AGENT_OUTPUT, MSG_IN, MSG_OUT, COMMUNE, COMMUNE_FAIL, plus TOOL_USE which is NAMED IN THE TAXONOMY AND HAS NO EMITTER (operator ruling 3 — its unlock is a per-harness digest capability, and naming it now is what keeps a later emitter from minting a second spelling). IO FRAMES ARE COMMAND-CLASS under the shell channel taxonomy (operator ruling 7): discrete, durable, spooled — they ride the shellchan spool path and MUST NOT route through drivehub or activity_write, which are the ephemeral latest-wins machinery ADR-0048 decision 2 defines for ACTIVITY frames. Two frame classes share one stream and share NO machinery; a future reader must not be able to reach an IO frame through the drive hub. PAYLOAD IS BOUNDED AT 16KB-CLASS with an explicit `truncated` flag and a digest `seq` pointer, because the digest stays the content surface and the frame is the push signal: a consumer that needs the untruncated body follows the seq. Truncation is UTF-8 SAFE (cut on a character boundary, never mid-codepoint) and a truncated frame is still a valid frame.
2026-09-06T13:09:02.3678507Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3678703Z 
2026-09-06T13:09:02.3678850Z ### REQ-IO-INGEST-STATE-PAYLOAD
2026-09-06T13:09:02.3685446Z - Title: THE BUSY/IDLE INGEST EDGE CARRIES AN OPTIONAL PAYLOAD, AND EMITTING IT NEVER TOUCHES THE IDLE-EDGE STAMP (doyle W1 rulings 1 and 2). `api state busy` carries the USER_INPUT payload and `api state idle` carries the AGENT_OUTPUT end-of-turn payload; the payload arrives on STDIN or via `--payload-file`, never as an inline argument (payloads are 16KB-class and an inline arg is the Windows command-length trap; `spt send` already reads bodies from stdin, so stdin is house precedent). SUPPLYING BOTH STDIN AND --payload-file IS AN IMMEDIATE NAMED REFUSAL — house style refuses ambiguity rather than guessing which the caller meant. THE PAYLOAD IS OPTIONAL AND ITS ABSENCE IS THE BACK-COMPAT ARM: a call with no payload behaves byte-for-byte as it does today and emits NO event, which is what lets every already-shipped adapter keep firing this verb on every hook across the release that lands this. THE DECOUPLING, which is the load-bearing decision: one IO event is emitted per PAYLOAD-CARRYING `api state` call, while `stamp_transition`'s edge-only rule is left byte-for-byte untouched. Binding it to the transition edge instead would mean an adapter that reports idle without ever reporting busy emits exactly ONE event for the life of the endpoint. The alternative of stamping every report is REFUSED BY NAME: ADR-0048 decision 3 anchors an idle countdown to the flip instant, and walking that instant forward on every hook fire is precisely the defect that decision exists to prevent, so a consumer countdown could never fire. Core does NOT dedupe calls — EXACTLY-ONCE SPAN REPORTING belongs to the adapter under the manifest IO-compliance declaration (respec'd releases#238, gated 2026-08-29): every reported payload span is reported exactly once across the turn, mid-turn spans and the closing remainder disjoint. The once-per-turn spelling this replaced was true only while end-of-turn was the sole AGENT_OUTPUT shape. A MID-TURN SPAN IS AN AGENT_OUTPUT REPORT ON THE BUSY ARM marked by the mid attribute — see REQ-IO-MIDTURN-SPAN, which owns that arm; this requirement keeps the payload plumbing and the decoupling.
2026-09-06T13:09:02.3691398Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.3691631Z 
2026-09-06T13:09:02.3691794Z ### REQ-IO-MSG-EDGES
2026-09-06T13:09:02.3694503Z - Title: MSG_IN AND MSG_OUT ARE ACCOUNTED AT THE DELIVERY EDGES CORE ALREADY OWNS, not at a new interception point: MSG_OUT publishes where the send path commits an outbound message and MSG_IN publishes where core delivers or injects an inbound one, so the funnel observes the edges rather than growing a parallel notion of what a message is. THE INGEST-SCOPE BOUNDARY IS ESTABLISHED HERE EVEN THOUGH THE PARSERS LAND LATER (operator ruling 11): a received message body is INGEST DATA and never a parse surface — no MSG_IN body may fire a dispatch or a ceremony at the receiver, and W1 is where MSG_IN gets published in a shape that keeps the later parsers structurally unable to read it as agent output. Emission is best-effort and never alters delivery: a message is delivered, spooled, or refused exactly as it is today regardless of what the funnel does with the observation.
2026-09-06T13:09:02.3697204Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3697422Z 
2026-09-06T13:09:02.3697577Z ### REQ-IO-COMMUNE-EVENT
2026-09-06T13:09:02.3702268Z - Title: A COMMUNE IS ACCOUNTED AT THE DELETION CORE ALREADY PERFORMS, CARRYING THE DROP FILE CONTENT VERBATIM (operator ruling 5). The event fires where `ingest_drops` consumes a commune drop and unlinks it — the seam core already owns — and its payload is the bytes of that file, never Psyche output, never echo-commune output, never the composed brief a resume renders from it. Wake markers (`!!wake!!`) are the harness adapter's construct and get ZERO special treatment here: core carries the text through untouched and does not parse, strip, or act on them. TWO DELETIONS EXIST AT THIS SEAM AND ONLY ONE IS A CONSUMPTION: the F-032 commit-before-delete branch (REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE) deliberately PRESERVES a commune drop whose project slice could not be committed, rewriting it in place for a later resolvable ingest, and deletes only a SIGNOFF source there — a drop on that branch has not been consumed, it is coming back, and emitting for it would double-fire when the real consumption later deletes the same content. Emission is therefore keyed on the CONSUMPTION OUTCOME the ingest already reports (`preserved` false) rather than on reaching a call to remove_file, so the preserved path is excluded structurally instead of by a condition a later edit can drift past. A DELETED SIGNOFF DROP EMITS NOTHING: the vocabulary has no SIGNOFF token and one is not borrowed. Emission never alters ingest: a drop is consumed, preserved, or left to retry exactly as it is today regardless of the funnel.
2026-09-06T13:09:02.3706510Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.3706818Z 
2026-09-06T13:09:02.3706944Z ### REQ-IO-COMMUNE-FAIL-EVENT
2026-09-06T13:09:02.3709958Z - Title: A FAILED COMMUNE INGEST EMITS A NAMED REASON AND LEAVES ITS FILE ON DISK (operator ruling 6). Today a failing ingest is a bare `PSYCHE_INGEST_FAIL` line on the daemon's stderr — the failure is real, the drop survives as the on-disk diagnostic, and NOBODY IS TOLD: the measured instance is six such lines across three agents in one day, produced by a shared-checkout git index.lock collision, none of which reached the agents whose context was not rebuilt. The event is the push signal that silence is missing; the surviving FILE remains the diagnostic and is never deleted by the failure path. The reason is CARRIED, not re-derived: the sweep already composes a named reason and already keeps the FIRST failure across drop dirs, and the event publishes that same string, so the event and the log line can never disagree about why. Emission is best-effort and never alters ingest — a failed ingest retries on the next pass exactly as it does today.
2026-09-06T13:09:02.3712600Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.3712823Z 
2026-09-06T13:09:02.3712952Z ### REQ-IO-SUPPRESSION-GRAMMAR
2026-09-06T13:09:02.3715806Z - Title: ONE SUPPRESSION GRAMMAR, CORE-OWNED, SHARED BY EVERY SHORTFORM (operator ruling 8). A shortform marker inside an inline backtick span or a fenced code block is a QUOTATION and fires nothing — that is how an agent writes about a tag without dispatching one, and how a code sample containing a marker stays a code sample. The grammar lives in ONE place and is applied by every shortform reader: the `@<…@>` parser of this wave and the `;;` seal-mint of the next, so the two can never disagree about what is quoted. A second spelling of suppression is the defect this requirement exists to prevent — an author who learns the rule from one surface must not be surprised by the other. The masking is computed over the raw text before any marker scan, so a marker is either inside a suppressed region or it is live, never both, and nesting is resolved by the outer region: a backtick span inside a fenced block is simply more fenced content.
2026-09-06T13:09:02.3718467Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3718801Z 
2026-09-06T13:09:02.3718925Z ### REQ-IO-SHORTFORM-GATE
2026-09-06T13:09:02.3722494Z - Title: SHORTFORM IS ENABLED PER-ADAPTER, ONLY ON A MANIFEST IO-COMPLIANCE DECLARATION (operator ruling 9), AND A DECLARING ADAPTER MAY STILL OPT OUT (ruling 8). Absent the declaration core parses NOTHING for that adapter, which is what makes the migration safe: every adapter shipping its own parser today keeps parsing, alone, until its own release declares compliance and deletes the local parser in the same change — so no release exists in which both parse the same text and dispatch it twice. THE DEFAULT IS OFF AND THAT IS LOAD-BEARING: a default-on core would double-fire against every already-shipped adapter the moment core upgraded, which is precisely the window ruling 9 forbids. The manifest fields are additive and N-1-safe (absent in older manifests, omitted from serialization when unset), so an older core reading a newer manifest and a newer core reading an older one both behave as they did before. Core owns the field and the gating; the handshake with each adapter's own release is the gater's boundary, not this requirement's.
2026-09-06T13:09:02.3725507Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.3725784Z 
2026-09-06T13:09:02.3725912Z ### REQ-IO-SHORTFORM-DISPATCH
2026-09-06T13:09:02.3728774Z - Title: CORE PARSES `@<targets body @>` OVER INGESTED AGENT OUTPUT AND USER INPUT ONLY, AND DISPATCHES THROUGH THE NORMAL SEND PATH (operator rulings 10 and 11). The grammar: `@<` opens, comma-separated target ids run to the first space, the body runs to the first `@>`. THE INGEST-SCOPE BOUNDARY IS ABSOLUTE AND STRUCTURAL: parsers run over USER_INPUT and AGENT_OUTPUT ingest only and NEVER over a MSG_IN body — a message that ARRIVES carrying a live tag can never fire a dispatch or a ceremony at its receiver, because a peer's text is data about a tag, not an instruction to send one. That boundary is held by which funnel path the body traverses rather than by a kind check a later edit could widen. Dispatch reuses the delivery path core already owns — no parallel send machinery, no second notion of what delivery means — so admission, sealing, spooling and refusal behave exactly as they do for an operator-typed send.
2026-09-06T13:09:02.3731556Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.3731795Z 
2026-09-06T13:09:02.3731923Z ### REQ-IO-DISPATCH-RESULTS
2026-09-06T13:09:02.3734665Z - Title: A SHORTFORM DISPATCH ACCUMULATES A PER-TARGET RESULT, AND THAT IS ITS ONLY CONFIRMATION CHANNEL (operator ruling 10). Each target of a parsed tag records what actually happened to it — delivered, or no perch — accumulated as the v1 DISPATCH_RESULTS category; the seal-mint outcomes and refusals of the next wave accumulate into the same category. RENDERING IS NOT THIS WAVE: the now-signal verb that surfaces these lands with #233, so this requirement delivers accumulation with no reader yet, deliberately — the same shape as the event bus shipping with one sink, and for the same reason, that the reader must cost a registration rather than a rework. No other confirmation channel may be added: an author learns what their dispatch did through the now-signal and nowhere else, so a dispatch must never grow its own bespoke echo, log line, or reply message as a substitute.
2026-09-06T13:09:02.3737169Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3737387Z 
2026-09-06T13:09:02.3737513Z ### REQ-IO-SEAL-SHORTFORM-GRAMMAR
2026-09-06T13:09:02.3744489Z - Title: THE `;;` SEAL-MINT SHORTFORM PARSES UNDER THE SAME SUPPRESSION GRAMMAR AS `@<…@>` (operator rulings 8 and 12-14). Pairs parse GREEDY LEFT-TO-RIGHT and each pair mints its OWN ceremony, run sequentially — two pairs in one turn are two seals over two texts, never one seal over the span between them. AN EMPTY PAIR `;;;;` IS FULLY IGNORED: no ceremony, and NO REFUSAL either, because an author who typed nothing between the markers asked for nothing and a refusal would be noise about a non-event. AN ODD TRAILING `;;` IS THE BARE CASE and seals ALL TEXT AFTER IT THROUGH END OF OUTPUT, with the text between the last completed pair and the bare marker left UNTOUCHED — the bare marker is a decision to seal the remainder, not a retroactive claim on what preceded it. Pair parsing runs FIRST and the bare case is what an odd marker becomes after pairing, which is the only order under which `;;a;; tail ;;rest` has one pair and one bare seal rather than an ambiguous three-marker soup. Detection covers AGENT OUTPUT and USER INPUT alike (CONTEXT.md): either author path mints, because the user typing the directive verbatim and the agent drafting it are the same intent. THE BARE CASE IS REACHABLE ONLY AT THE TURN-CLOSE INGEST (releases#238, design gated 2026-08-29): mid-turn, END OF OUTPUT names text the parser HAS NOT SEEN, so a per-chunk bare seal would silently seal a SHORTER region than the author asked for — a correctness failure indistinguishable from success in the output. In a mid span an odd trailing marker therefore mints NOTHING and is refused by name as SEAL_BARE_MIDTURN, routed into the shipped DISPATCH_RESULTS surface rather than a rival refusal beside it, with the refusal text naming the fix (close the pair, or place the bare marker in the turn's closing output). PAIRS are self-delimiting — both ends present in the chunk — and keep minting mid-turn exactly as ruled, which is what honours the operator's parse-all-agent-output ruling. At the idle turn-close ingest ruling 14 stands verbatim, because there end-of-output is text the parser holds in full. This narrowing costs core ZERO cross-call state, which is what keeps the no-turn-assembly boundary free of an exception.
2026-09-06T13:09:02.3750697Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.3750940Z 
2026-09-06T13:09:02.3751077Z ### REQ-IO-SEAL-SHORTFORM-CEREMONY
2026-09-06T13:09:02.3754783Z - Title: A `;;` MINT RUNS THE EXISTING SEAL CEREMONY, OFF THE HOOK PATH, AND ITS REFUSAL IS THE ONE ALREADY SHIPPED (operator ruling 15 + the async rider ruled 2026-08-27). The ceremony is `Brain::seal_ceremony` — the same TOTP human-presence path `spt seal mint` runs; no second ceremony, no second refusal. WITH NO CAPABLE CONTROLLER ATTACHED the ceremony already answers an immediate named `SEAL_NO_CEREMONY_SURFACE` with no pending state and no timeout queue, and W4 ROUTES that outcome into DISPATCH_RESULTS rather than minting a rival refusal beside it. INGEST-FIRED MINTS ARE ASYNC-DISPATCHED: the `api state` call returns immediately and the ceremony runs off the hook path, because that ceremony blocks for a human-scale duration and an adapter hook that blocks on a human overlay is the wedge class this project has refused twice already (the `--payload-stdin` sniff and the pump deadlines). THE TWO IMMEDIACIES ARE DIFFERENT AND BOTH HOLD: `immediate, never queued` pins CEREMONY SEMANTICS — nothing waits for a controller to show up — while the hook's return is PLUMBING, so an async hook does not weaken ruling 15. A reader must be able to see that distinction in the code rather than rediscover it.
2026-09-06T13:09:02.3758260Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.3758504Z 
2026-09-06T13:09:02.3758686Z ### REQ-IO-MIDTURN-SPAN
2026-09-06T13:09:02.3765739Z - Title: MID-TURN AGENT OUTPUT IS AN AGENT_OUTPUT EVENT CARRYING A mid ATTRIBUTE, NOT A NEW KIND (operator ruling, releases#238 comment 5461768445; design gated 2026-08-29). An adapter reports a span while the agent is BUSY, so the span arrives on the busy arm of api state and must be typed AGENT_OUTPUT there: state_io_kind gains a THIRD ARGUMENT and stays pure — busy+payload without the flag is USER_INPUT unchanged, busy+payload WITH it is AGENT_OUTPUT with mid set, idle+payload is AGENT_OUTPUT unchanged, and a payload-free call still emits nothing. THE FLAG AT IDLE IS A CONTRADICTION AND IS REFUSED BY NAME (STATE_MID_ON_IDLE), as is the flag with no payload; both are argument validation refused where STATE_PAYLOAD_AMBIGUOUS is refused, so the funnel's rule that an observation can never fail the operation it observes is untouched. THE ATTRIBUTE IS PRESENT-ONLY, spelled mid=1 on the frame in the house form truncated already uses, a skip-when-false row field, and a poll field emitted only when set; an enum spelling was refused because an enum invites an ORDINAL, and core cannot know a span's ordinal without modelling turn assembly, which the funnel refuses. THE ATTRIBUTE MUST BE MINTED AT EVERY SITE BECAUSE THE #234 READER DOES NOT PASS UNKNOWN ATTRIBUTES THROUGH — measured at sha 786d2381: IoLogRow carries no flatten and no deny_unknown_fields so serde ACCEPTS AND DISCARDS an unknown key, and render_json is an explicit per-field allowlist projection, so an unretained field could never reach an adapter. The attr-passthrough precedent is a fact about the EVENT wire, not about this reader. AN OLDER READER IS NOT VERSION-GATED: it reads a mid row as an ordinary end-of-turn report and therefore OVER-COUNTS TURNS, which is acceptable because nothing emits a span unless an adapter asks, a new kind is exactly what the operator ruled against, and the body an old reader gets is real agent output verbatim and in order. THAT NO-GATE ARGUMENT RESTS ENTIRELY ON UNKNOWN-FIELD TOLERANCE, so the forward-compat unit is REQUIRED rather than optional (gater rider, 2026-08-29): a stored row carrying a field this binary does not know must be READ, not skipped — unpinned, a later deny_unknown_fields would make parse_line answer None and the reader SKIP the row in silence, turning a forward-compatible record into a quiet poll.
2026-09-06T13:09:02.3772230Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.3772471Z 
2026-09-06T13:09:02.3772626Z ### REQ-IO-BOUNDARY-EVENTS
2026-09-06T13:09:02.3787040Z - Title: ENDPOINT BOUNDARIES ARE THEIR OWN FRAME CLASS ON THE WIRE, CARRIED BY THE SHIPPED IO FUNNEL UNDERNEATH (operator ruling releases#239 comment 5461768503; carrier design gated 2026-08-29). THE WIRE CATEGORY IS RULED AND NOT REOPENABLE: boot/clear/compact ship as a NEW boundary frame category with EVENT-CLASS SEMANTICS -- edges, ordered, each occurrence matters -- and explicitly NOT as an activity subset, because activity is drive-class latest-wins and would drop edges by construction. That premise is true at the IMPLEMENTATION and not merely in the doc that asserts it: write_activity is a single latest-wins slot and take_activity takes AND clears, so a clear followed by a compact before a poll loses the clear outright. THE THREE VARIANTS ARE MINTED WHOLE in one closed list and are NOT new spellings -- SessionTrigger::token() already answers exactly boot/clear/compact and already serves as the boundary marker kind in the spanned digest, so the emitter MAPS FROM THE TRIGGER rather than typing a literal, which is what keeps the wire and the ledger from drifting into two vocabularies for one concept. THE CARRIER WAS A SEPARATE DECISION AND IT WENT TO THE SHIPPED FUNNEL: same bus, same durable per-endpoint io log, same api io events cursor, NO TRIM CHANGE. A boundary-first trim ordering was proposed and REFUSED at the gate because it breaks the trim's own deciding arithmetic -- seqs are contiguous BECAUSE eviction is front-only, so skip-boundary eviction makes the row count overcount by every hole and fires a whole-file rewrite on every append past that point, which is the cost the trim slack exists to amortize. THE FRAME CLASS IS A FUNCTION OF THE KIND AND OF NOTHING ELSE: the two vocabularies are disjoint (SCREAMING_CASE io kinds, lowercase boundary variants, pinned by a test rather than left to habit), so the sink reads the wire class off the closed list with no second field to keep in sync. THE BODY IS EMPTY AND THAT IS LOAD-BEARING (the activity precedent, not the io one): a boundary's entire content is which edge and when, and there is no untruncated version of it living anywhere else. ONE EVENT PER REAL BOUNDARY: sessions::append answers Ok(false) for a same-session re-bind and calls that not a new boundary in its own words, so both emitters gate on it -- publishing off the ATTEMPT would emit an edge on a call that crossed none and make the funnel disagree with the ledger written by the same call. THE EMITTER PUBLISHES THE COERCED TOKEN, NEVER THE CALLER WORD: cmd_boundary reads an unknown mode as clear rather than refusing it, so the raw argument would put an unbounded string on a closed wire vocabulary. EMISSION IS BEST-EFFORT AND CAN NEVER FAIL THE BOUNDARY IT OBSERVES, the rule the boundary's own ledger append already holds. THE OBSERVATION CONTRACT IS A DOC DELIVERABLE, NOT A NICETY (gate rider): a live-attached shell receives the pushed frame, a session-cursor poller sees every boundary from its seed forward, and history before first contact is reachable only by an explicit --after and only within the retention window -- boot in particular is observable live and by an early --after and is NOT replayed to a silently-seeded consumer. Unstated, the carrier's one real limit is discoverable only by someone who already knows it, which is how an undiscoverable shipped surface re-files itself as a bug.
2026-09-06T13:09:02.3795738Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.3795908Z 
2026-09-06T13:09:02.3796028Z ### REQ-IO-DISPATCH-RESULT-VOCABULARY
2026-09-06T13:09:02.3799118Z - Title: THE DISPATCH_RESULTS ROW VOCABULARY IS MINTED WHOLE, INCLUDING THE ARM NOTHING EMITS YET (ruled 2026-08-27, on the TOOL_USE pattern). One closed outcome vocabulary covers every confirmation the funnel carries: DELIVERED, NO_PERCH, NOT_DELIVERED with the refusing exit code, and the seal-mint outcome and refusal arms. NO_PERCH IS NAMED BUT NOT EMITTED until the W5 lane's typed send-path return can distinguish it — today `cmd_send` answers 1 for a missing perch, an ambiguous target and a refused ring alike, so NOT_DELIVERED carries that coarse code as a TRANSITIONAL arm. Naming the unemitted arm now is the point, exactly as the event taxonomy named TOOL_USE without an emitter: a constant nothing produces costs nothing, while a second vocabulary minted later costs every consumer and turns W5 into a migration instead of a one-arm change. A ROW ALSO NEED NOT NAME A TARGET: a seal outcome is about text, not a recipient, so the target is optional and a consumer must not assume one.
2026-09-06T13:09:02.3801802Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3801971Z 
2026-09-06T13:09:02.3802070Z ### REQ-IO-SEND-VERDICT
2026-09-06T13:09:02.3806774Z - Title: THE SEND PATH RETURNS A NAMED OUTCOME, NOT AN EXIT CODE, AND THAT IS WHAT POPULATES NO_PERCH (booked as the W5 prerequisite, IO-PARSER #22). Before this, cmd_send_with_body answered 1 for a missing perch, a refused admission gate, an ambiguous target, an unconfirmed WAN hop and an empty body alike, so the DISPATCH_RESULTS accumulator could only record the coarse transitional NOT_DELIVERED arm and the ruled delivered/no-perch split was NOT DERIVABLE AT ALL — a report this lane handed the gater by name rather than approximating. The send path therefore names its own outcomes: DELIVERED covers SENT and QUEUED alike (both mean the message is the target's problem now), NO_PERCH covers every absence arm across all three delivery cascades — the local perch that is not listening, the WAN leg answering no-perch-remote, and the WAN registry answering not-found — and REFUSED carries the exit code for every outcome that is a refusal rather than an absence. THE EXIT CODE IS DERIVED FROM THE VERDICT AND NEVER THE OTHER WAY AROUND: the CLI wrapper maps the verdict to its process code so that the operator-facing contract is unchanged to the byte, while the in-process caller reads the name. AN ABSENCE IS NOT A REFUSAL, AND THE OPPOSITE READING IS THE ONE THIS EXISTS TO KILL: an author whose peer simply is not listening must not be told their message was refused, because those two facts call for different next actions. NO ROW ALREADY WRITTEN IS MIGRATED — the store is append-only, so previously-written NOT_DELIVERED rows stay exactly as written and the arm merely narrows for rows written from here on.
2026-09-06T13:09:02.3811267Z - Required stages: impl, unit
2026-09-06T13:09:02.3811424Z 
2026-09-06T13:09:02.3811519Z ### REQ-NOW-SIGNAL-VERB
2026-09-06T13:09:02.3814674Z - Title: spt api now-signal IS THE ONE SITUATIONAL-AWARENESS FUNNEL (ratified 2026-07-29, access-control grill; releases#233). One verb answers the question an agent asks at the top of every turn — what changed that I should know about — and it renders per-category XML tags nested under a single SPT-NOW-SIGNAL root. The hint verb SURVIVES AS A THIN ALIAS over the HINTS category and gains no independent behaviour, because two verbs answering the same question is how an adapter ends up injecting both and an agent ends up trusting whichever it happened to read. NO SECOND INJECTION FUNNEL MAY BE ADDED BESIDE IT (the design of record names this as the thing to avoid): a surface that wants to reach an agent at turn boundaries registers a CATEGORY here rather than growing its own injection point. The render is composed by ONE composer over a category list, so a category cannot invent its own envelope grammar, and an EMPTY render emits NOTHING AT ALL — not an empty root, because an empty root is a payload that costs context on every poll and says nothing.
2026-09-06T13:09:02.3817677Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.3817849Z 
2026-09-06T13:09:02.3817940Z ### REQ-NOW-SIGNAL-DELTA
2026-09-06T13:09:02.3821732Z - Title: EVERY NOW-SIGNAL CATEGORY IS DELTA-ONLY, ON PER-SESSION SEEN-SETS, AND A POLL WITH NOTHING NEW EMITS NOTHING (ratified 2026-07-29). This is the keyword-hint mechanism GENERALIZED rather than re-invented per category: each category records what this session has already been shown and reports only what it has not. THE EMPTINESS IS THE CONTRACT, NOT AN ACCIDENT OF THERE BEING NO DATA — the adapter guidance is to inject on every UserPromptSubmit- and PreToolUse-equivalent, which is only affordable if a quiet poll costs zero context, so a category that re-emits an already-shown datum is a defect even though its output is TRUE. THE SEEN-SET IS PER SESSION, not per endpoint and not global: a new session has seen nothing and is entitled to the current picture once, which is what makes the first poll of a session useful without making every later poll noisy. A DESCRIPTION-CLASS DATUM IS SHOWN ONCE PER SESSION by the same mechanism rather than by a bespoke flag. Proving this needs a NON-VACUOUS FIRST POLL BESIDE THE EMPTY SECOND ONE: an assertion that the second poll is empty, standing alone, passes just as well against a verb that emits nothing ever, so the two polls are asserted together as one test.
2026-09-06T13:09:02.3825056Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.3825219Z 
2026-09-06T13:09:02.3825313Z ### REQ-NOW-SIGNAL-SPEC
2026-09-06T13:09:02.3827805Z - Title: THE HARNESS ADAPTER CAN PRESCRIBE AND TUNE CATEGORIES AT POLL TIME VIA --spec-manifest and --spec-file (ratified 2026-07-29). An adapter knows things core cannot: which categories its harness can render, which are noise in its surface, and what its injection budget is. --spec-manifest reads the tuning the runtime manifest already carries; --spec-file takes the same shape as JSON on disk for an adapter that composes it per poll. A SPEC NARROWS AND TUNES; IT DOES NOT INVENT — a category not in the v1 vocabulary cannot be conjured by a spec, so the vocabulary stays the one closed list every consumer reads. AN ABSENT, UNREADABLE OR MALFORMED SPEC IS THE DEFAULT PICTURE, NEVER A REFUSAL: this verb runs on a hook at every turn boundary, and a poll that fails hard on a bad config file is a hook that breaks a working session over a typo.
2026-09-06T13:09:02.3830148Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3830302Z 
2026-09-06T13:09:02.3830407Z ### REQ-NOW-SIGNAL-CATEGORIES-V1
2026-09-06T13:09:02.3834520Z - Title: THE v1 CATEGORY SET IS CLOSED AND EACH CATEGORY ANSWERS ONE QUESTION (ratified 2026-07-29 + operator ruling 10). HINTS carries the keyword-hint payload the alias verb used to. ENDPOINT_MENTIONS fires when the author's words exactly match a known endpoint name and answers existence, online status, node, shared subnets, and the description ONCE PER SESSION. MONICS surfaces the standing judgements a matched trigger raised — the second ratified consumer of the trigger vocabulary, which is why that vocabulary was ratified whole rather than sender-only. SHELLS reports shell instances and adapters, and SUBSUMES AND DEPRECATES the session-start spt-shells message, because the same datum arriving through two channels is the ambiguity this funnel exists to end. LAST_MSGS reports the last outgoing and incoming message as time, relative-to-now, peer, and a short excerpt — enough to re-ground after a reset without re-reading a transcript. EDGE_TRANSITIONS reports endpoint and node on/offline edges and subnet joins. DISPATCH_RESULTS renders the accumulated shortform per-target delivered/no-perch rows and the seal-mint outcomes and refusals, and IS THE ONLY CONFIRMATION SURFACE FOR THEM (ruling 10) — the reader the W3 and W4 accumulators were built to cost a registration rather than a rework. EACH CATEGORY OBEYS THE DELTA DISCIPLINE; none may render an unconditional static payload.
2026-09-06T13:09:02.3838317Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.3838483Z 
2026-09-06T13:09:02.3838584Z ### REQ-NOW-SIGNAL-UPDATES
2026-09-06T13:09:02.3842908Z - Title: THE UPDATES CATEGORY SURFACES RUNNING VERSIONS FOR THREE SUBJECTS, AND THE DELTA DISCIPLINE IS ITS EVENT DETECTOR (releases#245, operator-linked at the #23 intake). The subjects are the ticket's three: spt-core itself, the endpoint's current harness adapter, and the adapter of every shell currently registered to the endpoint -- keyed PER SHELL, not per adapter, because an agent acts on the shell and a version that moved under one instance is the fact it needs. NO EVENT JOURNAL IS BUILT, and that is the design rather than an omission: each subject's seen-set key CARRIES its version, so a session is told once at the version it first observed and hears nothing again until that version CHANGES, at which point the key is new and the line fires. An update event IS a version that differs from the one this session was told about, and the seen-set already answers that question exactly; a separate journal would be a second source for one fact. THE PRODUCT VERSION LEADS FOR spt-core, never the applied-update counter: the counter is a different axis (update-set sequence) from the version an operator speaks, and an agent can only act on the version it is running. ABSENCE IS SILENCE: an endpoint with no update history emits nothing, because no-updates is not an update, and a shell whose adapter is deregistered or unreadable contributes NOTHING rather than an error line -- this rides a turn-boundary hook, where a diagnostic in the payload is noise the author cannot act on. The category is POST-v1: it does not widen the ratified v1 set on our own authority, it is appended after it by operator ruling, and render order is declaration order so the ratified order is untouched.
2026-09-06T13:09:02.3847270Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3847432Z 
2026-09-06T13:09:02.3847527Z ### REQ-NOW-SIGNAL-SEAL-BRIEF
2026-09-06T13:09:02.3850725Z - Title: THE SEAL_BRIEF CATEGORY TEACHES SEALING IN AT MOST TWO SHORT SENTENCES, ONCE PER SESSION (releases#18, ruled by the operator at the access-control grill follow-up 2026-07-29; the two-sentence bound is the operator constraint verbatim and is not ours to relax). IT ANSWERS EXACTLY TWO THINGS, in that order: what sealing IS and its significance as a PROVEN USER DIRECTIVE, and how the agent EXECUTES the proof. THE COMMAND IT NAMES MUST BE THE REAL ONE -- a payload whose entire purpose is telling an agent how to act is the worst possible place for an invented verb, and the first draft of this text cited a `spt seal verify` that does not exist; the shipped surface is `spt api seal verify <token>` with the content on stdin, mint via the `;;text;;` shortform. THE TEXT IS A CONSTANT, so the wording has ONE home and the two-sentence bound is auditable at a glance rather than reconstructed from a formatter. ONCE PER SESSION on the standing delta discipline: an agent that has been told does not need telling again, and a brief repeating every turn is precisely the noise that discipline exists to prevent. The category is POST-v1, appended by operator ruling rather than by widening the ratified set.
2026-09-06T13:09:02.3854009Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3854160Z 
2026-09-06T13:09:02.3854256Z ### REQ-SHELL-HINTS
2026-09-06T13:09:02.3861716Z - Title: A SHELL ADAPTER'S [[hints]] ARE LIVE TEXT, READ THROUGH THE NOW-SIGNAL HINTS CATEGORY UNDER A PER-SOURCE CAP (releases#133, operator-authored and operator-GREENLIT into NOW-SIGNAL #23; the cap shape ruled by the gater 2026-08-30). THE SCHEMA WAS NEVER THE GAP: hints is a top-level manifest field with no kind gate, so a kind=shell manifest has always parsed, validated, registered and profile-merged its [[hints]] -- what was missing is a READER, because the only reader resolves the ENDPOINT-BOUND manifest and therefore never looked at any other adapter. THE ONE-HINT-PER-MESSAGE CLAUSE (CONTEXT.md keyword hints, ratified 2026-06-12) IS AMENDED BY REPLACEMENT TO A PER-SOURCE CAP: the harness contributes at most one line and EACH shell adapter contributes at most one line, each still once per session. The authority chain is explicit: the clause was ratified when exactly ONE manifest could speak, its ground was flood protection from a single adapter's chatter, and #133 cannot be delivered under a global cap at all -- so the amendment implements the newer operator decision while preserving the original ground per source, where a global cap would convert every second adapter into a structurally mute one that the ground never asked for. TWO ARMS, DECIDED BY INSTANTIATION AND NEVER BY LINK STATE: an adapter this owner holds an instance of surfaces the FULL hint text regardless of whether that instance is online or offline, and an adapter with no instance surfaces only a teaser naming the trigger keyword and the command that shows the text. THE TEASER'S COMMAND MUST EXIST AT THE HEAD THAT SHIPS IT -- the same class as the SEAL_BRIEF phantom verb, and the shipped surface is spt adapter hints <adapter[:profile]>, verified against the built binary rather than remembered. THE SEEN-SET KEY GAINS ADAPTER IDENTITY, because the existing key hashes only keywords, text and the regex flag, so two adapters shipping identical hint text would otherwise share one entry and the first to fire would silence the other invisibly for the session. THE HARNESS KEY SPELLING IS UNCHANGED: qualification is additive and shell-side only, so no session's seen-set forgets what it has already been told when this lands. The shell key also carries its ARM, so a shell that is instantiated after its teaser fired still delivers the full text once -- a teaser tells the agent the hint exists and acting on it must not be punished by silence. ABSENCE IS SILENCE: no shells, no matching keyword, an unreadable or deregistered adapter, or an endpoint bound to no harness manifest at all each contribute NOTHING, and in particular the shell arm is NOT gated on the harness manifest being present, because an endpoint with no bound manifest can still own shells. THE DOC STAGE LANDS WHERE A SHELL-ADAPTER AUTHOR LOOKS (docs-site shells) rather than only in the ratification text, because a manifest section no author can find is a section that reads as absent.
2026-09-06T13:09:02.3869820Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.3870007Z 
2026-09-06T13:09:02.3870115Z ### REQ-NOW-SIGNAL-EDGE-SUBJECT-NAMING
2026-09-06T13:09:02.3872997Z - Title: THE EDGE_TRANSITIONS SUBNET-JOIN LINE NAMES ITS SUBJECT AS A NODE AND LABELS ITS FALLBACK AS A PUBKEY (releases#11b render rider, ruled 2026-08-30). The join arm reads `RosterEntry`, which is a NODE record (node pubkey, subnet, and a `label` that is the member's OS hostname) -- so the arm is NODE-LEVEL ONLY: there is no endpoint-level join here to distinguish, and a node does not join a subnet FROM another node. THE TICKET'S `z joined <subnet> from <node>` WORDING NAMES AN UNBUILT DATA SOURCE, NOT A MIS-RENDER, and the from-node half went back to the board as its own request rather than being invented here or silently dropped -- that distinction is the whole reason this requirement exists, because the next reader of the ticket would otherwise re-derive it. WHAT IS FIXABLE IN THE RENDER, AND IS FIXED: the line says `node <label> joined subnet <x>` so it states what its subject is, and the unlabeled fallback renders as `pubkey <12hex>...` rather than as a bare hex prefix standing where a name goes, because a bare prefix reads as an endpoint id to every reader who has not opened the function.
2026-09-06T13:09:02.3876126Z - Required stages: impl, unit
2026-09-06T13:09:02.3876278Z 
2026-09-06T13:09:02.3876383Z ### REQ-NOW-SIGNAL-DEFERRED-CATEGORIES
2026-09-06T13:09:02.3877752Z - Title: PROJECTS (spt-bs-releases#16) AND FILE_ACCESS_HELPER (#17) ARE MINTED AS DEFERRED NOW-SIGNAL CATEGORIES SO THEY ARE NOT LOST (ratified 2026-07-29). They are NAMED, NOT BUILT: v1 ships without them deliberately, and this requirement exists so that the decision is on the record as a deferral with an issue behind it rather than as an omission a later reader mistakes for an oversight. Activating this requirement is what the lane that builds them does; until then it carries no stages and fails nothing.
2026-09-06T13:09:02.3879503Z - Required stages: 
2026-09-06T13:09:02.3879637Z 
2026-09-06T13:09:02.3879746Z ### REQ-IO-EVENT-ADAPTER-LOG
2026-09-06T13:09:02.3885102Z - Title: ADAPTER-CONSUMABLE IO EVENTS LAND IN A PER-ENDPOINT APPEND-ONLY LOG REGISTERED AS A THIRD BUS SINK (releases#234, operator ruling 2). The funnel's claim was that a second reader costs a REGISTRATION and never a rework, and on the sink side that holds exactly as claimed — this consumer is one `bus.register` line in `default_bus`. THE READER IS FREE, THE STORE IS NOT, and this requirement is the honest half of that claim: neither existing sink writes a per-endpoint, ordered, cursorable surface — the shell-link sink spools per linked shell and the last-msg sink keeps two overwritten slots — so a delta-cursored poll needs a NEW DURABLE STORE, and that store is what this covers. EVERY ROW CARRIES ITS OWN MONOTONIC seq AS THE LINE'S KEY RATHER THAN AS A JSON FIELD: a row is `<seq>` TAB `<json>`, so a cursor scan parses an integer prefix and never the body, and the ordering key cannot become an accident of serialization field order. THE LOG'S seq AND THE DIGEST seq ARE DIFFERENT NUMBERS AND ARE SPELLED DIFFERENTLY (`seq` versus `digest_seq`), because the digest remains the content surface that a truncated payload points at and one name for two counters is a consumer following the wrong one. THE LOG IS BOUNDED PER ENDPOINT AND TRIMMED OLDEST-FIRST so that an adapter which stops polling cannot grow it without limit; the bound is a STATED CHOICE derived from a measured event rate rather than a guessed number, and the trim is amortized against a slack so an ordinary append is not a whole-file rewrite. APPENDS ARE SERIALIZED under an exclusive advisory lock on a stable sentinel, because the daemon publishes from several edges and two racing appends must not mint a colliding seq. A SINK FAILURE IS STILL ONLY A REPORT: this store may not become the first sink whose bad day reaches the operation it observes.
2026-09-06T13:09:02.3890048Z - Required stages: doc, impl, unit
2026-09-06T13:09:02.3890211Z 
2026-09-06T13:09:02.3890311Z ### REQ-IO-EVENT-POLL-VERB
2026-09-06T13:09:02.3899462Z - Title: spt api io-events IS THE DELTA-CURSORED POLL A HARNESS ADAPTER READS IO EVENTS THROUGH (releases#234; the operator DELEGATED the mechanics and CHOSE POLL over push). It answers with the rows the caller has not yet been shown and with nothing else. TWO CURSOR MODES OVER ONE ORDERING: `--session-id <sid>` keeps a per-session cursor exactly as `api now-signal` keeps per-session seen-sets, and `--after <seq>` lets a caller carry its own cursor exactly as `endpoint digest --after` already does; naming both is what keeps a stateless adapter and a session-keyed hook off two different verbs. THE CURSOR KEY IS THE AUTH SESSION ID AND NOT A SECOND FLAG BESIDE IT: the harness session is ONE identity, and a `--session` for the cursor sitting one character from a `--session-id` for the gate would be two ways to be wrong about it on a verb an adapter wires once; a token-authenticated caller has no session identity and uses `--after`. A POLL WITH NEITHER CURSOR IS REFUSED BY NAME (`IO_EVENTS_NO_CURSOR`, exit 2) RATHER THAN ANSWERED WITH SILENCE, because a caller who asked an unanswerable question must not read the answer as nothing having happened. A NEW SESSION'S FIRST POLL SEES NOTHING AND SEEDS ITS CURSOR SILENTLY — history is the digest's job, and replaying an unbounded backlog into a turn-boundary hook is the exact cost the now-signal's delta discipline exists to avoid, with EDGE_TRANSITIONS the standing precedent for seeding silently for that reason. PROVING THIS NEEDS THE SEEDED-EMPTY FIRST POLL ASSERTED BESIDE A NON-EMPTY SECOND ONE, because an assertion that the first poll is empty passes just as well against a verb that emits nothing ever. ALL SIX EMITTED KINDS ARE VISIBLE — USER_INPUT, AGENT_OUTPUT, MSG_IN, MSG_OUT, COMMUNE, COMMUNE_FAIL — and AN UNKNOWN KIND IS IGNORED RATHER THAN REFUSED, the same posture the now-signal category vocabulary takes toward a name it does not know. TOOL_USE STAYS UNEMITTED AND THIS VERB DOES NOT CHANGE THAT: measurement says the harness adapter is its natural emitter, which is a question back to deployah and then the operator and must not ride in on this verb. THE PAYLOAD BOUND IS THE 16KB CLASS WITH A truncated FLAG AND THE DIGEST POINTER, MATCHING THE SHELL FRAME AS A CHOICE AND NOT AS AN INHERITANCE — `IoEvent.payload` is deliberately unbounded at the bus layer and the cap belongs to the frame — so that one event reads identically through either transport and a consumer needing the whole body follows the pointer into the digest. THE POLL IS AUTHENTICATED THE WAY `api poll` IS, AND FOR THE SAME REASON: it hands back the session's VERBATIM user input and agent output, which is the payload class addressed to the endpoint's occupant rather than to whoever asks. This is a DELIBERATE DEPARTURE from its sibling reader `api now-signal`, which is ungated because it renders DERIVED summaries — a ten-word excerpt, a category count — and never a raw payload; the gate follows the content, not the verb family. Proof is the `--session-id` an adapter already passes to `api state`, or a capability token, so the gate costs a compliant adapter nothing.
2026-09-06T13:09:02.3907759Z - Required stages: doc, impl, unit, int
2026-09-06T13:09:02.3907940Z 
2026-09-06T13:09:02.3908039Z ### REQ-EMIT-SINGLE-WRITE
2026-09-06T13:09:02.3908517Z - Title: A machine-parsed diagnostic emission is handed to the OS as exactly ONE write of the complete rendered text including its terminating newline.
2026-09-06T13:09:02.3909109Z - Required stages: impl, unit
2026-09-06T13:09:02.3909265Z 
2026-09-06T13:09:02.3909357Z ## How to report back
2026-09-06T13:09:02.3909480Z 
2026-09-06T13:09:02.3909638Z For every (requirement, failing criterion) pair, emit one finding:
2026-09-06T13:09:02.3909865Z 
2026-09-06T13:09:02.3909950Z     {
2026-09-06T13:09:02.3910135Z       "code": "requirement_quality",
2026-09-06T13:09:02.3910364Z       "requirementId": "REQ-...",
2026-09-06T13:09:02.3910645Z       "criterion": "singular" | "verifiable" | "atomic" | "active-voice",
2026-09-06T13:09:02.3910946Z       "message": "<short reason>",
2026-09-06T13:09:02.3911178Z       "suggestedRevision": "<optional rewrite>"
2026-09-06T13:09:02.3911478Z     }
2026-09-06T13:09:02.3911585Z 
2026-09-06T13:09:02.3911756Z Wrap your response as { "findings": [ ... ] } listing only your concerns; the
2026-09-06T13:09:02.3912147Z deterministic findings above don't need to be repeated.
